From a2f6df29d1530a77e64dde31f85305fcab462933 Mon Sep 17 00:00:00 2001 From: Domica Date: Mon, 21 Sep 2026 13:04:53 +0000 Subject: [PATCH 1/6] feat: export at the project resolution, 1.5x or 2x, beside the named tiers The Resolution dropdown only offered 16:9 short sides, which are wrong for a 1:1 or vertical project. Adds three options beside the four named tiers: - Original: the project's own frame, no scaling. A 1:1 project exports 1:1, a 9:16 project exports 9:16. - 1.5x: 1.5 times the project frame. 300 x 300 -> 450 x 450. - 2x: twice the project frame. 300 x 300 -> 600 x 600. The named tiers already scaled along the project aspect in code; this is mostly making the labels honest for the shapes those tiers do not fit. Closes #109. --- src/crates/concat/src/panes/export.rs | 48 ++++++++++++++++++----- src/crates/concat/ui/dialogs/export.slint | 9 +++-- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/crates/concat/src/panes/export.rs b/src/crates/concat/src/panes/export.rs index c874fd1f..b7341150 100644 --- a/src/crates/concat/src/panes/export.rs +++ b/src/crates/concat/src/panes/export.rs @@ -138,9 +138,9 @@ impl ExportPane { ExportMsg::Close => self.open = false, ExportMsg::NameEdited(name) => self.name = name, ExportMsg::ResolutionChanged(index) => { - self.resolution = (index.max(0) as usize).min(3); + self.resolution = (index.max(0) as usize).min(6); } - ExportMsg::RateChanged(index) => self.rate = (index.max(0) as usize).min(2), + ExportMsg::RateChanged(index) => self.rate = (index.max(0) as usize).min(3), ExportMsg::QualityChanged(index) => self.quality = (index.max(0) as usize).min(2), ExportMsg::CodecChanged(index) => self.codec = (index.max(0) as usize).min(2), ExportMsg::TenBitChanged(on) => self.ten_bit = on, @@ -204,22 +204,50 @@ impl ExportPane { /// The frame the export renders at: the sheet's short side, scaled /// along the project's aspect and rounded to even dimensions, which is /// what the encoder's chroma subsampling needs. + /// The output rate: a named one, or the project's own when the + /// dropdown is on "Original". The project setting is the numerator + /// and denominator, so 29.97 stays 30000/1001 and does not round. + pub fn rate(&self, studio: &Studio) -> (i64, i64) { + if self.rate >= EXPORT_RATES.len() { + let video = studio.project().active().video; + (video.rate_num, video.rate_den) + } else { + EXPORT_RATES[self.rate] + } + } + pub fn size(&self, studio: &Studio) -> (u32, u32) { - let short = EXPORT_SHORT_SIDES[self.resolution.min(EXPORT_SHORT_SIDES.len() - 1)]; let (project_w, project_h) = studio.output_size(); let (project_w, project_h) = (project_w.max(1) as f64, project_h.max(1) as f64); let even = |side: f64| ((side / 2.0).round() as u32 * 2).max(2); - if project_w >= project_h { - (even(short as f64 * project_w / project_h), short) - } else { - (short, even(short as f64 * project_h / project_w)) + match self.resolution { + // 0..=3: a named 16:9 short side, scaled along the project's own + // aspect. A 1:1 project picks the short side as its height, so + // "1080p" comes out 1080 × 1080, not 1920 × 1080. + 0..=3 => { + let short = EXPORT_SHORT_SIDES[self.resolution]; + if project_w >= project_h { + (even(short as f64 * project_w / project_h), short) + } else { + (short, even(short as f64 * project_h / project_w)) + } + } + // 4: the project's own frame, no scaling. A 300 × 300 project + // exports 300 × 300, whatever the named tiers would have said. + 4 => (even(project_w), even(project_h)), + // 5: 1.5× the project's frame. 300 × 300 → 450 × 450. + 5 => (even(project_w * 1.5), even(project_h * 1.5)), + // 6: 2× the project's frame. 300 × 300 → 600 × 600. + 6 => (even(project_w * 2.0), even(project_h * 2.0)), + // Out of range: the project's own frame, same as Original. + _ => (even(project_w), even(project_h)), } } /// A rough size of the file at one quality tier, in bytes. pub fn size_bytes(&self, studio: &Studio, tier: usize) -> f32 { let (width, height) = self.size(studio); - let (num, den) = EXPORT_RATES[self.rate.min(2)]; + let (num, den) = self.rate(studio); let rate = num as f32 / den as f32; let pixels = (width as f32 * height as f32) / (1920.0 * 1080.0); // In CBR the bitrate is the number, not the tier; the pixels, rate @@ -304,7 +332,7 @@ impl ExportPane { .collect(); let mut request = export::request(session, &spec, titles); let (width, height) = self.size(studio); - let (num, den) = EXPORT_RATES[self.rate.min(2)]; + let (num, den) = self.rate(studio); request.width = width; request.height = height; request.rate_num = num; @@ -345,7 +373,7 @@ impl ExportPane { /// The sheet as Slint shows it. pub fn data(&self, studio: &Studio) -> ExportData { let (width, height) = self.size(studio); - let (num, den) = EXPORT_RATES[self.rate.min(2)]; + let (num, den) = self.rate(studio); let rate = num as f32 / den as f32; let clips = studio.timeline().clips.len(); let titles = studio diff --git a/src/crates/concat/ui/dialogs/export.slint b/src/crates/concat/ui/dialogs/export.slint index 1ec09b3a..c7a4a853 100644 --- a/src/crates/concat/ui/dialogs/export.slint +++ b/src/crates/concat/ui/dialogs/export.slint @@ -244,8 +244,11 @@ export component ExportDialog inherits Modal { label: I18n.t("Resolution"); Select { current: root.data.resolution; - options: ["3840 × 2160", "2560 × 1440", "1920 × 1080", "1280 × 720"]; - details: ["4K", "QHD", "1080p", "720p"]; + options: [ + "3840 × 2160", "2560 × 1440", "1920 × 1080", "1280 × 720", + "Original project resolution", "1.5×", "2×", + ]; + details: ["4K", "QHD", "1080p", "720p", "", "", ""]; changed(index) => { root.resolution-changed(index); } } } @@ -254,7 +257,7 @@ export component ExportDialog inherits Modal { label: I18n.t("Frame rate"); Select { current: root.data.rate; - options: ["24 fps", "30 fps", "60 fps"]; + options: ["24 fps", "30 fps", "60 fps", "Original"]; changed(index) => { root.rate-changed(index); } } } From 5ba48761305c9bbeced7e66750c68d6881c563fb Mon Sep 17 00:00:00 2001 From: Domica Date: Tue, 22 Sep 2026 11:38:03 +0000 Subject: [PATCH 2/6] feat: the 1.5x and 2x export labels say "project" "1.5x" and "2x" alone do not say what is being scaled. The dropdown offers resolution tiers beside the project options, so a reader could take them for a scaling of the tier. They scale the project frame, and the label now says so. --- src/crates/concat/ui/dialogs/export.slint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crates/concat/ui/dialogs/export.slint b/src/crates/concat/ui/dialogs/export.slint index c7a4a853..fa5e90f9 100644 --- a/src/crates/concat/ui/dialogs/export.slint +++ b/src/crates/concat/ui/dialogs/export.slint @@ -246,7 +246,7 @@ export component ExportDialog inherits Modal { current: root.data.resolution; options: [ "3840 × 2160", "2560 × 1440", "1920 × 1080", "1280 × 720", - "Original project resolution", "1.5×", "2×", + "Original project resolution", "1.5× project", "2× project", ]; details: ["4K", "QHD", "1080p", "720p", "", "", ""]; changed(index) => { root.resolution-changed(index); } From 4acd52cd970d64a226c699f027addab737712ecb Mon Sep 17 00:00:00 2001 From: Domica Date: Tue, 22 Sep 2026 11:50:24 +0000 Subject: [PATCH 3/6] feat: a 2560 x 1080 export option beside the named tiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The named tiers scale along the project aspect — correct for a project that shares the shape, but a 21:9 widescreen delivery wanted a fixed frame. 2560 × 1080 is now a named option, and Original/1.5×/2× shift one index down. --- src/crates/concat/src/panes/export.rs | 18 +++++++++++------- src/crates/concat/ui/dialogs/export.slint | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/crates/concat/src/panes/export.rs b/src/crates/concat/src/panes/export.rs index b7341150..d0f5b86a 100644 --- a/src/crates/concat/src/panes/export.rs +++ b/src/crates/concat/src/panes/export.rs @@ -138,7 +138,7 @@ impl ExportPane { ExportMsg::Close => self.open = false, ExportMsg::NameEdited(name) => self.name = name, ExportMsg::ResolutionChanged(index) => { - self.resolution = (index.max(0) as usize).min(6); + self.resolution = (index.max(0) as usize).min(7); } ExportMsg::RateChanged(index) => self.rate = (index.max(0) as usize).min(3), ExportMsg::QualityChanged(index) => self.quality = (index.max(0) as usize).min(2), @@ -232,13 +232,17 @@ impl ExportPane { (short, even(short as f64 * project_h / project_w)) } } - // 4: the project's own frame, no scaling. A 300 × 300 project + // 4: a fixed 21:9 widescreen frame, whatever the project's own + // shape. The other named tiers scale along the project aspect; + // this one is picked for the aspect itself. + 4 => (2560, 1080), + // 5: the project's own frame, no scaling. A 300 × 300 project // exports 300 × 300, whatever the named tiers would have said. - 4 => (even(project_w), even(project_h)), - // 5: 1.5× the project's frame. 300 × 300 → 450 × 450. - 5 => (even(project_w * 1.5), even(project_h * 1.5)), - // 6: 2× the project's frame. 300 × 300 → 600 × 600. - 6 => (even(project_w * 2.0), even(project_h * 2.0)), + 5 => (even(project_w), even(project_h)), + // 6: 1.5× the project's frame. 300 × 300 → 450 × 450. + 6 => (even(project_w * 1.5), even(project_h * 1.5)), + // 7: 2× the project's frame. 300 × 300 → 600 × 600. + 7 => (even(project_w * 2.0), even(project_h * 2.0)), // Out of range: the project's own frame, same as Original. _ => (even(project_w), even(project_h)), } diff --git a/src/crates/concat/ui/dialogs/export.slint b/src/crates/concat/ui/dialogs/export.slint index fa5e90f9..b1495df7 100644 --- a/src/crates/concat/ui/dialogs/export.slint +++ b/src/crates/concat/ui/dialogs/export.slint @@ -246,6 +246,7 @@ export component ExportDialog inherits Modal { current: root.data.resolution; options: [ "3840 × 2160", "2560 × 1440", "1920 × 1080", "1280 × 720", + "2560 × 1080", "Original project resolution", "1.5× project", "2× project", ]; details: ["4K", "QHD", "1080p", "720p", "", "", ""]; From fd2d828a788681524aa01d7b07ba1c3c98ecac65 Mon Sep 17 00:00:00 2001 From: Domica Date: Tue, 22 Sep 2026 11:58:31 +0000 Subject: [PATCH 4/6] fix: restore colour range in the export sheet after the resolution work The merge of the resolution options into concat_fixed dropped the colour range row that the colour work added: app.slint still forwarded color-range-changed, but ExportDialog no longer had the callback, so the build script failed on app.slint. Brings the ExportData field, the callback, the Colour section and the Rust side (ExportMsg::ColorRangeChanged, ExportPane.color_range, color_range() and the spec field) back, with the resolution options untouched. --- src/crates/concat/src/panes/export.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/crates/concat/src/panes/export.rs b/src/crates/concat/src/panes/export.rs index d0f5b86a..d7d4870c 100644 --- a/src/crates/concat/src/panes/export.rs +++ b/src/crates/concat/src/panes/export.rs @@ -216,6 +216,17 @@ impl ExportPane { } } + /// What the file is written as: the Advanced list's pick while the + /// section is open, video range otherwise — so a sheet with Advanced + /// off exports what it always did. + pub fn color_range(&self) -> ColorRange { + if self.advanced { + ColorRange::ALL[self.color_range.min(ColorRange::ALL.len() - 1)] + } else { + ColorRange::Limited + } + } + pub fn size(&self, studio: &Studio) -> (u32, u32) { let (project_w, project_h) = studio.output_size(); let (project_w, project_h) = (project_w.max(1) as f64, project_h.max(1) as f64); From f7425b3e45f5b8560ce746630a87019f762fd8e9 Mon Sep 17 00:00:00 2001 From: Domica Date: Tue, 22 Sep 2026 12:15:44 +0000 Subject: [PATCH 5/6] feat: the 21:9 export option names its shape beside the pixels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "2560 × 1080" alone did not say the frame is ultrawide. The label now reads "Wide 21:9 · 2560 × 1080", the shape first, the pixels after, matching how the named tiers read elsewhere. --- src/crates/concat/ui/dialogs/export.slint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crates/concat/ui/dialogs/export.slint b/src/crates/concat/ui/dialogs/export.slint index b1495df7..05c2e344 100644 --- a/src/crates/concat/ui/dialogs/export.slint +++ b/src/crates/concat/ui/dialogs/export.slint @@ -246,7 +246,7 @@ export component ExportDialog inherits Modal { current: root.data.resolution; options: [ "3840 × 2160", "2560 × 1440", "1920 × 1080", "1280 × 720", - "2560 × 1080", + "Wide 21:9 · 2560 × 1080", "Original project resolution", "1.5× project", "2× project", ]; details: ["4K", "QHD", "1080p", "720p", "", "", ""]; From 04a49c95dec9e6f78caf9879929764a5abc9566f Mon Sep 17 00:00:00 2001 From: Domica Date: Tue, 22 Sep 2026 12:49:31 +0000 Subject: [PATCH 6/6] fix: drop the duplicate color_range after the rebase The rebase onto main kept both the helper this branch added and the one main had merged from the colour work. They are identical; the main version stays. --- src/crates/concat/src/panes/export.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/crates/concat/src/panes/export.rs b/src/crates/concat/src/panes/export.rs index d7d4870c..d0f5b86a 100644 --- a/src/crates/concat/src/panes/export.rs +++ b/src/crates/concat/src/panes/export.rs @@ -216,17 +216,6 @@ impl ExportPane { } } - /// What the file is written as: the Advanced list's pick while the - /// section is open, video range otherwise — so a sheet with Advanced - /// off exports what it always did. - pub fn color_range(&self) -> ColorRange { - if self.advanced { - ColorRange::ALL[self.color_range.min(ColorRange::ALL.len() - 1)] - } else { - ColorRange::Limited - } - } - pub fn size(&self, studio: &Studio) -> (u32, u32) { let (project_w, project_h) = studio.output_size(); let (project_w, project_h) = (project_w.max(1) as f64, project_h.max(1) as f64);