diff --git a/Cargo.lock b/Cargo.lock index 6911da6a5..247142a45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1038,12 +1038,12 @@ dependencies = [ [[package]] name = "craft-docpath" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" +source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" [[package]] name = "craft-pdf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" +source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" dependencies = [ "hayro-syntax", "thiserror 2.0.21", @@ -1052,7 +1052,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" +source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" dependencies = [ "egui", "log", @@ -1065,7 +1065,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" +source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" dependencies = [ "chrono", "dunce", @@ -2870,7 +2870,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" [[package]] name = "hayro-jbig2" version = "0.3.0" -source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" +source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" dependencies = [ "hayro-ccitt", ] @@ -2884,7 +2884,7 @@ checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" +source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" dependencies = [ "hayro-jbig2", "hayro-jpeg2000", diff --git a/Cargo.toml b/Cargo.toml index 0e1fd2cd5..fe6d2ddcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ rust-version = "1.95" repository = "https://github.com/storytold/effectcraft" [workspace.dependencies] -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", version = "=0.1.0", default-features = false, features = ["read"] } -craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", features = ["egui"] } +craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", version = "=0.1.0", default-features = false, features = ["read"] } +craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3" } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", features = ["egui"] } # internal effectcraft-time = { path = "crates/time" } effectcraft-geom = { path = "crates/geom" } @@ -108,7 +108,7 @@ pollster = "1" # ui # Shared frontend components; keep this dependency above the engine boundary. # Shared UI and the complete egui family select reviewed immutable commits below. -craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", features = ["serde"] } +craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", features = ["serde"] } # `default_fonts` and `monochrome_emoji_fonts` are explicit: the app's `FontDefinitions::default()` # and its `✔`/`▣` chrome glyphs need both, and kittest's own dev-dependencies do not propagate. egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] } @@ -220,8 +220,8 @@ lto = "thin" codegen-units = 1 [patch.crates-io] -hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", version = "=0.7.2" } -hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", version = "=0.3.0" } +hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", version = "=0.7.2" } +hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", version = "=0.3.0" } ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } diff --git a/crates/pdf/README.md b/crates/pdf/README.md index 9801c0574..7389b00c8 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -138,7 +138,7 @@ ffmpeg elsewhere) to check the code tables against an independent implementation ## Shared parser setup and bounds The workspace pins `craft-pdf` to `craft-libs` Git revision -`1e6f127844ec61b3b8afdcc2bb76e7cfe470153f`, with `default-features = false` and only the explicit `read` +`512b54355cd1418f4e8c56a3fbd7483c3c274bf3`, with `default-features = false` and only the explicit `read` feature. This excludes the backend's bundled fonts and Adobe CMaps and preserves EffectCraft's font and asset policies. The shared parser does not change the supported drawing operators, image codecs, continuously rasterized SVG tree, editable shapes, or EPS interpreter. diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 40ce78b9b..054aa3598 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -527,69 +527,129 @@ pub enum DocAction { Close(u64), } -const DOC_PAD: f32 = 8.0; -const DOC_SWATCH: f32 = 14.0; +/// Room around a tab's label, as craft-ui gives its dock tabs. +const TAB_PADDING: f32 = 24.0; +/// Width of the » button that lists the comp tabs with no room in the strip. +const DOC_OVERFLOW: f32 = 22.0; +/// Room for the × after the shown comp's name. const DOC_CLOSE: f32 = 16.0; -fn doc_width(ui: &egui::Ui, doc: &DocTab) -> f32 { - let label = ui.painter().layout_no_wrap(doc.title.clone(), Tokens::ui(12.0), egui::Color32::WHITE).size().x; - DOC_PAD + DOC_SWATCH + label + if doc.active { DOC_CLOSE } else { 0.0 } + DOC_PAD +/// A comp tab's text: the label swatch, then the name, as the panel tabs with a swatch show it. +fn doc_job(doc: &DocTab, font: &egui::FontId, color: egui::Color32) -> egui::text::LayoutJob { + let mut job = egui::text::LayoutJob::default(); + job.append("\u{25a0} ", 0.0, egui::TextFormat::simple(font.clone(), doc.swatch)); + job.append(&doc.title, 0.0, egui::TextFormat::simple(font.clone(), color)); + job } -/// The width the comp tabs ask for in the Timeline's tab strip. -pub fn doc_tabs_width(ui: &egui::Ui, docs: &[DocTab]) -> f32 { - docs.iter().map(|d| doc_width(ui, d)).sum() +/// The width of one line of `job` at the fonts' current size. +fn text_width(ctx: &egui::Context, job: egui::text::LayoutJob) -> f32 { + ctx.fonts_mut(|f| f.layout_job(job)).size().x } -/// Draw the Timeline's comp tabs into `strip` (in its group header): the comp's label swatch, its -/// name, and × on the shown one. Tabs past the strip's end are left out; Shift+, and Shift+. step -/// through every open comp. Automation: `panel.tab.Timeline.`, and `panel.tab.Timeline.close` -/// for the shown comp's ×. -pub fn doc_tabs(ui: &mut egui::Ui, strip: Rect, docs: &[DocTab], t: &Tokens, reg: &mut crate::automation::Registry) -> Vec { +/// Whether comp tab `doc` carries the ×: the comp the Timeline shows, while it is the group's +/// shown panel. +fn has_close(doc: &DocTab, selected: bool) -> bool { + selected && doc.active +} + +/// The width each comp tab asks for in the Timeline group's tab row. +pub fn doc_widths(ctx: &egui::Context, docs: &[DocTab], selected: bool, font: &egui::FontId) -> Vec { + docs.iter().map(|d| text_width(ctx, doc_job(d, font, egui::Color32::WHITE)) + TAB_PADDING + if has_close(d, selected) { DOC_CLOSE } else { 0.0 }).collect() +} + +/// Draw the Timeline's comp tabs into `strip`, the part of the Timeline group's tab row after its +/// panel tabs. Each is a tab like the panel tabs: the comp's label swatch and name, and × on the +/// shown comp while the Timeline is the group's shown panel (`selected`). Tabs that do not fit go +/// to a » menu at the end; the shown comp's tab always stays in the row. Shift+, and Shift+. step +/// through every open comp. Automation: `panel.tab.Timeline.`, `panel.tab.Timeline.close` +/// for the shown comp's ×, `panel.tab.Timeline.more` for », and `panel.tab.Timeline.more.` +/// for its items. +pub fn doc_tabs( + ui: &mut egui::Ui, + strip: Rect, + docs: &[DocTab], + selected: bool, + style: &craft_ui::docking::DockStyle, + reg: &mut crate::automation::Registry, +) -> Vec { let mut out = Vec::new(); - let painter = ui.painter().clone(); - let mut x = strip.min.x; + let timeline = PanelKind::Timeline.id(); + let widths = doc_widths(ui.ctx(), docs, selected, &style.font); + let active = docs.iter().position(|d| d.active).unwrap_or(0); + let fit = craft_ui::tabs::fit_whole(&widths, active, strip.width(), DOC_OVERFLOW); + let clip = strip.intersect(ui.clip_rect()); let mid = strip.center().y; - for doc in docs { - let w = doc_width(ui, doc); - if x + w > strip.max.x { - break; - } - let tab = Rect::from_min_size(pos2(x, strip.min.y), vec2(w, strip.height())); - let resp = ui.interact(tab, egui::Id::unique(("comp-tab", doc.id)), Sense::click()); - reg.add_response(&format!("panel.tab.{}.{}", PanelKind::Timeline.id(), doc.id), &resp, &doc.title); - if doc.active { - painter.rect_filled(tab, 0.0, t.panel_bg); - } - let sw = Rect::from_center_size(pos2(tab.min.x + DOC_PAD + 5.0, mid), vec2(9.0, 9.0)); - painter.rect_filled(sw, 1.0, doc.swatch); - let color = if doc.active || resp.hovered() { t.tab_text_active } else { t.tab_text }; - let galley = painter.layout_no_wrap(doc.title.clone(), Tokens::ui(12.0), color); - let label_x = tab.min.x + DOC_PAD + DOC_SWATCH; - let label_w = galley.size().x; - painter.galley(pos2(label_x, mid - galley.size().y / 2.0), galley, color); - if doc.active { - painter.line_segment([pos2(label_x, strip.max.y - 3.0), pos2(label_x + label_w, strip.max.y - 3.0)], Stroke::new(1.0, t.tab_text_active)); - let cr = Rect::from_center_size(pos2(label_x + label_w + DOC_CLOSE / 2.0 + 2.0, mid), vec2(10.0, 10.0)); - let close = ui.interact(cr.expand(2.0), egui::Id::unique(("comp-tab-close", doc.id)), Sense::click()); - let cc = if close.hovered() { t.tab_text_active } else { t.tab_text }; + let mut x = strip.min.x; + for (index, width) in &fit.shown { + let Some(doc) = docs.get(*index) else { continue }; + let tab = Rect::from_min_size(pos2(x, strip.min.y), vec2(*width, strip.height())); + x += width; + let on = selected && doc.active; + let color = if on { style.text } else { style.inactive_text }; + let close = has_close(doc, selected); + let mut job = doc_job(doc, &style.font, color); + job.wrap = egui::text::TextWrapping { + max_width: (tab.width() - TAB_PADDING - if close { DOC_CLOSE } else { 0.0 }).max(0.0), + max_rows: 1, + break_anywhere: true, + overflow_character: Some('\u{2026}'), + }; + let galley = ui.fonts_mut(|f| f.layout_job(job)); + let label_end = tab.min.x + TAB_PADDING * 0.5 + galley.size().x; + let resp = craft_ui::tabs::Tab::new(egui::Id::unique(("comp-tab", doc.id)), &doc.title, on).focus_stroke(Stroke::new(1.0, style.accent)).show_at( + ui, + tab, + |ui, response| { + let painter = ui.painter().with_clip_rect(tab.intersect(clip)); + painter.rect_filled(tab, 0.0, if on || response.hovered() { style.active_background } else { style.tab_background }); + painter.galley(pos2(tab.min.x + TAB_PADDING * 0.5, mid - galley.size().y * 0.5), galley.clone(), color); + }, + ); + let resp = if galley.elided { resp.on_hover_text(&doc.title) } else { resp }; + reg.add_response(&format!("panel.tab.{timeline}.{}", doc.id), &resp, &doc.title); + if close { + let cr = Rect::from_center_size(pos2(label_end + DOC_CLOSE / 2.0, mid), vec2(10.0, 10.0)); + let hit = ui.interact(cr.expand(2.0), egui::Id::unique(("comp-tab-close", doc.id)), Sense::click()); + hit.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, hit.enabled(), "Close")); + let cc = if hit.hovered() { style.text } else { style.inactive_text }; let k = 3.5; + let painter = ui.painter().with_clip_rect(tab.intersect(clip)); painter.line_segment([cr.center() + vec2(-k, -k), cr.center() + vec2(k, k)], Stroke::new(1.2, cc)); painter.line_segment([cr.center() + vec2(-k, k), cr.center() + vec2(k, -k)], Stroke::new(1.2, cc)); - reg.add_response(&format!("panel.tab.{}.close", PanelKind::Timeline.id()), &close, "Close"); - crate::widgets::focus_outline(ui, &close, t); - if close.clicked() { + reg.add_response(&format!("panel.tab.{timeline}.close"), &hit, "Close"); + if hit.clicked() { out.push(DocAction::Close(doc.id)); } } - crate::widgets::focus_outline(ui, &resp, t); if resp.clicked() { out.push(DocAction::Activate(doc.id)); } if resp.middle_clicked() { out.push(DocAction::Close(doc.id)); } - x += w; + } + if !fit.overflow.is_empty() && fit.overflow_width > 0.0 { + let more = Rect::from_min_size(pos2(x, strip.min.y), vec2(fit.overflow_width, strip.height())); + let resp = ui.interact(more, egui::Id::unique("comp-tabs-more"), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, resp.enabled(), "More compositions")); + reg.add_response(&format!("panel.tab.{timeline}.more"), &resp, "More compositions"); + let color = if resp.hovered() { style.text } else { style.inactive_text }; + ui.painter().with_clip_rect(more.intersect(clip)).text(more.center(), Align2::CENTER_CENTER, "\u{bb}", style.font.clone(), color); + egui::Popup::menu(&resp).show(|ui| { + egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { + for index in &fit.overflow { + let Some(doc) = docs.get(*index) else { continue }; + let item = ui.button(doc_job(doc, &style.font, style.text)); + item.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, item.enabled(), &doc.title)); + reg.add_response(&format!("panel.tab.{timeline}.more.{}", doc.id), &item, &doc.title); + if item.clicked() { + out.push(DocAction::Activate(doc.id)); + ui.close(); + } + } + }); + }); } out } diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index 0381c83ea..41977e667 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -436,13 +436,13 @@ impl EffectcraftApp { let mut child = ui.new_child(egui::UiBuilder::new().max_rect(body).id_salt("effectcraft-dock-area")); child.set_clip_rect(body.intersect(ui.clip_rect())); let docs = self.tab_docs(); - let doc_width = dock::doc_tabs_width(ui, &docs); let ime = NativeIme::new(&layout.floating, &self.unpublished_viewports); let mut content = EffectContent { titles: self.tab_titles(), swatches: self.tab_swatches(), docs, - doc_width, + ctx: ctx.clone(), + style: style.clone(), app: self, tokens: t, doc_actions: Vec::new(), @@ -632,8 +632,9 @@ struct EffectContent<'a> { titles: Vec<(PanelKind, String)>, swatches: Vec<(PanelKind, egui::Color32)>, docs: Vec, - /// The width the comp tabs take in the Timeline's tab strip. - doc_width: f32, + /// Measures the comp tabs for their width in the tab row. + ctx: egui::Context, + style: DockStyle, doc_actions: Vec, /// Viewer locks clicked. locks: Vec, @@ -750,8 +751,12 @@ fn window_bodies(window: &Floating) -> usize { impl EffectContent<'_> { fn header_width(&self, active: PanelKind, strip_width: f32) -> f32 { let lock = if has_lock(active) { LOCK_W } else { 0.0 }; - let docs = if active == PanelKind::Timeline && !self.docs.is_empty() { self.doc_width } else { 0.0 }; - (docs + lock + MENU_W).min(strip_width * 2.0 / 3.0).max(MENU_W) + (lock + MENU_W).min(strip_width * 2.0 / 3.0).max(MENU_W) + } + + /// Whether the group of `panels` shows comp tabs: it holds the Timeline and comps are open. + fn has_doc_tabs(&self, panels: &[PanelKind]) -> bool { + panels.contains(&PanelKind::Timeline) && !self.docs.is_empty() } } @@ -785,8 +790,8 @@ impl DockContent for EffectContent<'_> { } /// The tab names what the panel shows ("Composition Intro"), after the label swatch of its - /// comp or layer. With comps open, the Timeline's own tab is just "Timeline"; its comp tabs - /// are in the group header. + /// comp or layer. With comps open, the Timeline's own tab is just "Timeline"; each comp has a + /// tab of its own after the group's panel tabs. fn label(&mut self, _ui: &egui::Ui, panel: &PanelKind, logical: &str, font: &egui::FontId, color: egui::Color32) -> egui::WidgetText { let title = self.titles.iter().find(|(p, _)| p == panel).map_or(logical, |(_, s)| s.as_str()); let Some((_, swatch)) = self.swatches.iter().find(|(p, _)| p == panel) else { return title.into() }; @@ -800,24 +805,18 @@ impl DockContent for EffectContent<'_> { self.header_width(*active, strip_width) } - /// The Timeline's comp tabs, the viewer lock and the ≡ panel menu of the shown panel. + /// The viewer lock and the ≡ panel menu of the shown panel. fn group_header(&mut self, ui: &mut egui::Ui, _panels: &[PanelKind], active: &PanelKind, _floating: bool) { let t = self.tokens; let header = ui.max_rect(); let menu = Rect::from_min_max(pos2((header.max.x - MENU_W).max(header.min.x), header.min.y), header.max); - let mut end = menu.min.x; if has_lock(*active) { - let lock = Rect::from_center_size(pos2(end - LOCK_W / 2.0, header.center().y), vec2(14.0, 14.0)); - end -= LOCK_W; + let lock = Rect::from_center_size(pos2(menu.min.x - LOCK_W / 2.0, header.center().y), vec2(14.0, 14.0)); let locked = self.app.ui.locked_tabs.contains(&active.id()); if dock::lock_button(ui, lock, *active, locked, &t, &mut self.app.auto) { self.locks.push(*active); } } - if *active == PanelKind::Timeline && !self.docs.is_empty() { - let strip = Rect::from_min_max(header.min, pos2(end.max(header.min.x), header.max.y)); - self.doc_actions.extend(dock::doc_tabs(ui, strip, &self.docs, &t, &mut self.app.auto)); - } let rect = Rect::from_center_size(menu.center(), vec2(18.0, 18.0)); let response = ui.interact(rect, egui::Id::unique(("panel-menu-button", active.id())), Sense::click()); response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, response.enabled(), "panel menu")); @@ -830,4 +829,19 @@ impl DockContent for EffectContent<'_> { self.menu = Some((*active, rect.left_bottom())); } } + + fn group_extra_tabs_width(&self, panels: &[PanelKind], active: &PanelKind, _floating: bool) -> f32 { + if !self.has_doc_tabs(panels) { + return 0.0; + } + dock::doc_widths(&self.ctx, &self.docs, *active == PanelKind::Timeline, &self.style.font).iter().sum() + } + + /// The Timeline's comp tabs, after its group's panel tabs whichever panel is shown. + fn group_extra_tabs(&mut self, ui: &mut egui::Ui, panels: &[PanelKind], active: &PanelKind, _floating: bool) { + if self.has_doc_tabs(panels) { + let selected = *active == PanelKind::Timeline; + self.doc_actions.extend(dock::doc_tabs(ui, ui.max_rect(), &self.docs, selected, &self.style, &mut self.app.auto)); + } + } } diff --git a/crates/ui-egui/tests/it/ui_tabs.rs b/crates/ui-egui/tests/it/ui_tabs.rs index 52d3258e1..65b429f44 100644 --- a/crates/ui-egui/tests/it/ui_tabs.rs +++ b/crates/ui-egui/tests/it/ui_tabs.rs @@ -424,3 +424,45 @@ fn a_tab_dropped_on_a_frame_joins_it() { fn group_of(w: &Workspace, p: PanelKind) -> Option> { Some(w.group_of(p)).filter(|g| !g.is_empty()) } + +fn has(h: &Harness<'_, EffectcraftApp>, id: &str) -> bool { + h.state().auto.previous.iter().chain(h.state().auto.elements.iter()).any(|e| e.id == id) +} + +#[test] +fn open_comps_are_tabs_in_the_timeline_row_and_the_rest_are_in_its_overflow_menu() { + let mut s = Session::default(); + let names = ["Opening Titles", "Lower Third Left", "Lower Third Right", "Product Shot", "End Card Wide", "Logo Sting Short"]; + let comps: Vec = names + .iter() + .map(|name| s.execute("comp.new", json!({"name": name, "width": 320, "height": 180, "duration": 4})).unwrap()["comp"].as_u64().unwrap()) + .collect(); + s.state.open_comps = vec![ItemId(comps[0])]; + s.state.active_comp = Some(ItemId(comps[0])); + let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).build_eframe(|_| EffectcraftApp::new(s)); + h.run_steps(3); + let alone = rect(&h, "panel.tab.RenderQueue"); + h.state_mut().session.state.open_comps = comps.iter().map(|c| ItemId(*c)).collect(); + h.run_steps(3); + // The panel tabs keep their width; the comp tabs follow them in the same row, in order. + let (tl, rq) = (rect(&h, "panel.tab.Timeline"), rect(&h, "panel.tab.RenderQueue")); + assert!((rq.width() - alone.width()).abs() < 0.5, "Render Queue {rq:?} was {alone:?}"); + let shown: Vec = comps.iter().copied().filter(|c| has(&h, &format!("panel.tab.Timeline.{c}"))).collect(); + assert!(shown.contains(&comps[0]) && shown.len() < comps.len(), "{shown:?}"); + let mut x = rq.max.x; + for c in &shown { + let tab = rect(&h, &format!("panel.tab.Timeline.{c}")); + assert!(tl.y_range().contains(tab.center().y) && tab.min.x >= x - 0.5, "comp {c} at {tab:?} after x {x}"); + x = tab.max.x; + } + // A comp with no room is in the » menu; picking it shows it, and its tab joins the row. + let hidden = comps.iter().copied().find(|c| !shown.contains(c)).unwrap(); + let more = rect(&h, "panel.tab.Timeline.more"); + assert!(more.min.x >= x - 0.5 && tl.y_range().contains(more.center().y), "{more:?}"); + click_n(&mut h, more.center(), 1); + let item = rect(&h, &format!("panel.tab.Timeline.more.{hidden}")); + click_n(&mut h, item.center(), 1); + assert_eq!(h.state().session.state.active_comp, Some(ItemId(hidden))); + assert_eq!(open_comps(&h), comps); + assert!(has(&h, &format!("panel.tab.Timeline.{hidden}"))); +} diff --git a/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index 6ba8bb759..c26876897 100644 --- a/xtask/src/pdf_deps.rs +++ b/xtask/src/pdf_deps.rs @@ -5,7 +5,7 @@ use std::process::Command; use serde_json::Value; -const REV: &str = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f"; +const REV: &str = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3"; const PACKAGES: &[(&str, &str)] = &[("craft-pdf", "0.1.0"), ("hayro-syntax", "0.7.2"), ("hayro-jbig2", "0.3.0")]; pub fn run(root: &Path) -> Result<(), String> {