From 1c7fbfd503c7318d41cb8c27f0d840741f0536f9 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 02:23:51 +0000 Subject: [PATCH 1/3] Timeline: each open comp is a tab in the group's tab row, with an overflow menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comp tabs follow the panel tabs and are drawn like them (craft-ui's tab widget and the dock style's font and colours). The panel tabs keep their full width; comps without room are listed in a » menu at the end of the row, and picking one shows it. The shown comp's tab always stays in the row, and the comp tabs stay while another panel of the group is shown. --- crates/ui-egui/src/dock.rs | 146 ++++++++++++++++++++--------- crates/ui-egui/src/dock_ui.rs | 72 +++++++++----- crates/ui-egui/tests/it/ui_tabs.rs | 42 +++++++++ 3 files changed, 195 insertions(+), 65 deletions(-) diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 40ce78b9b..6cbd3475d 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. +pub const TAB_PADDING: f32 = 24.0; +/// Width of the » button that lists the comp tabs with no room in the strip. +pub 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. +pub 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..8089211f9 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 tab labels for the group header's width. + ctx: egui::Context, + style: DockStyle, doc_actions: Vec, /// Viewer locks clicked. locks: Vec, @@ -748,10 +749,37 @@ 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) + /// The header holds the shown panel's lock and ≡ at the strip's end and, in the Timeline's + /// group, the comp tabs before them. craft-ui puts the header after the group's drag bar, so + /// the comp tabs follow the panel tabs only when the header takes all the room the panel tabs + /// and the bar leave. The panel tabs keep their full width; comps without room go to ». + fn header_width(&self, panels: &[PanelKind], active: PanelKind, strip_width: f32) -> f32 { + let tools = if has_lock(active) { LOCK_W } else { 0.0 } + MENU_W; + if self.docs.is_empty() || !panels.contains(&PanelKind::Timeline) { + return tools.min(strip_width * 2.0 / 3.0).max(MENU_W); + } + let tabs: f32 = panels.iter().map(|p| self.tab_width(p)).sum(); + // Half a point keeps rounding from shrinking the panel tabs. + let room = strip_width - tabs - self.style.drag_bar_width - 0.5; + room.max(tools + dock::DOC_OVERFLOW).min(strip_width).max(0.0) + } + + /// A panel tab's width as craft-ui lays it out: its label and the tab padding. + fn tab_width(&self, panel: &PanelKind) -> f32 { + let logical = self.logical_label(panel); + let (font, color) = (&self.style.font, self.style.text); + dock::text_width(&self.ctx, self.tab_job(panel, &logical, font, color)) + dock::TAB_PADDING + } + + /// A panel tab's text: what the panel shows, after the label swatch of its comp or layer. + fn tab_job(&self, panel: &PanelKind, logical: &str, font: &egui::FontId, color: egui::Color32) -> egui::text::LayoutJob { + let title = self.titles.iter().find(|(p, _)| p == panel).map_or(logical, |(_, s)| s.as_str()); + let mut job = egui::text::LayoutJob::default(); + if let Some((_, swatch)) = self.swatches.iter().find(|(p, _)| p == panel) { + job.append("■ ", 0.0, egui::TextFormat::simple(font.clone(), *swatch)); + } + job.append(title, 0.0, egui::TextFormat::simple(font.clone(), color)); + job } } @@ -785,23 +813,22 @@ 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() }; - let mut job = egui::text::LayoutJob::default(); - job.append("■ ", 0.0, egui::TextFormat::simple(font.clone(), *swatch)); - job.append(title, 0.0, egui::TextFormat::simple(font.clone(), color)); - job.into() + if self.swatches.iter().any(|(p, _)| p == panel) { + return self.tab_job(panel, logical, font, color).into(); + } + self.titles.iter().find(|(p, _)| p == panel).map_or(logical, |(_, s)| s.as_str()).into() } - fn group_header_width(&self, _panels: &[PanelKind], active: &PanelKind, _floating: bool, strip_width: f32) -> f32 { - self.header_width(*active, strip_width) + fn group_header_width(&self, panels: &[PanelKind], active: &PanelKind, _floating: bool, strip_width: f32) -> f32 { + self.header_width(panels, *active, strip_width) } - /// The Timeline's comp tabs, 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) { + /// The Timeline's comp tabs (in its group, whichever panel is shown), then 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); @@ -814,9 +841,10 @@ impl DockContent for EffectContent<'_> { self.locks.push(*active); } } - if *active == PanelKind::Timeline && !self.docs.is_empty() { + if panels.contains(&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 selected = *active == PanelKind::Timeline; + self.doc_actions.extend(dock::doc_tabs(ui, strip, &self.docs, selected, &self.style, &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()); 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}"))); +} From fce4a88c925a564c175c382ef1f0620a4a05b3ad Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 02:44:56 +0000 Subject: [PATCH 2/3] Timeline: comp tabs draw through craft-ui's extra-tabs hook Pin craft-libs crates to 3723e32a, which adds the group extra-tabs hook. The comp tabs follow the panel tabs directly, with no drag bar between them, and the group header holds only the lock and the panel menu. --- Cargo.lock | 12 +++--- Cargo.toml | 12 +++--- crates/pdf/README.md | 2 +- crates/ui-egui/src/dock.rs | 6 +-- crates/ui-egui/src/dock_ui.rs | 80 +++++++++++++++-------------------- xtask/src/pdf_deps.rs | 2 +- 6 files changed, 50 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6911da6a5..644e0fa05 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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" [[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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" 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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" 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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" 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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" 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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" dependencies = [ "hayro-jbig2", "hayro-jpeg2000", diff --git a/Cargo.toml b/Cargo.toml index 0e1fd2cd5..b0907c1fb 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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", version = "=0.1.0", default-features = false, features = ["read"] } +craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b" } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", 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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", 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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", version = "=0.7.2" } +hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", 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..1be4b0fe5 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` +`3723e32ab104bfb3b73fb207dd06a212f8a30e4b`, 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 6cbd3475d..054aa3598 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -528,9 +528,9 @@ pub enum DocAction { } /// Room around a tab's label, as craft-ui gives its dock tabs. -pub const TAB_PADDING: f32 = 24.0; +const TAB_PADDING: f32 = 24.0; /// Width of the » button that lists the comp tabs with no room in the strip. -pub const DOC_OVERFLOW: f32 = 22.0; +const DOC_OVERFLOW: f32 = 22.0; /// Room for the × after the shown comp's name. const DOC_CLOSE: f32 = 16.0; @@ -543,7 +543,7 @@ fn doc_job(doc: &DocTab, font: &egui::FontId, color: egui::Color32) -> egui::tex } /// The width of one line of `job` at the fonts' current size. -pub fn text_width(ctx: &egui::Context, job: egui::text::LayoutJob) -> f32 { +fn text_width(ctx: &egui::Context, job: egui::text::LayoutJob) -> f32 { ctx.fonts_mut(|f| f.layout_job(job)).size().x } diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index 8089211f9..41977e667 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -632,7 +632,7 @@ struct EffectContent<'a> { titles: Vec<(PanelKind, String)>, swatches: Vec<(PanelKind, egui::Color32)>, docs: Vec, - /// Measures tab labels for the group header's width. + /// Measures the comp tabs for their width in the tab row. ctx: egui::Context, style: DockStyle, doc_actions: Vec, @@ -749,37 +749,14 @@ fn window_bodies(window: &Floating) -> usize { } impl EffectContent<'_> { - /// The header holds the shown panel's lock and ≡ at the strip's end and, in the Timeline's - /// group, the comp tabs before them. craft-ui puts the header after the group's drag bar, so - /// the comp tabs follow the panel tabs only when the header takes all the room the panel tabs - /// and the bar leave. The panel tabs keep their full width; comps without room go to ». - fn header_width(&self, panels: &[PanelKind], active: PanelKind, strip_width: f32) -> f32 { - let tools = if has_lock(active) { LOCK_W } else { 0.0 } + MENU_W; - if self.docs.is_empty() || !panels.contains(&PanelKind::Timeline) { - return tools.min(strip_width * 2.0 / 3.0).max(MENU_W); - } - let tabs: f32 = panels.iter().map(|p| self.tab_width(p)).sum(); - // Half a point keeps rounding from shrinking the panel tabs. - let room = strip_width - tabs - self.style.drag_bar_width - 0.5; - room.max(tools + dock::DOC_OVERFLOW).min(strip_width).max(0.0) + fn header_width(&self, active: PanelKind, strip_width: f32) -> f32 { + let lock = if has_lock(active) { LOCK_W } else { 0.0 }; + (lock + MENU_W).min(strip_width * 2.0 / 3.0).max(MENU_W) } - /// A panel tab's width as craft-ui lays it out: its label and the tab padding. - fn tab_width(&self, panel: &PanelKind) -> f32 { - let logical = self.logical_label(panel); - let (font, color) = (&self.style.font, self.style.text); - dock::text_width(&self.ctx, self.tab_job(panel, &logical, font, color)) + dock::TAB_PADDING - } - - /// A panel tab's text: what the panel shows, after the label swatch of its comp or layer. - fn tab_job(&self, panel: &PanelKind, logical: &str, font: &egui::FontId, color: egui::Color32) -> egui::text::LayoutJob { - let title = self.titles.iter().find(|(p, _)| p == panel).map_or(logical, |(_, s)| s.as_str()); - let mut job = egui::text::LayoutJob::default(); - if let Some((_, swatch)) = self.swatches.iter().find(|(p, _)| p == panel) { - job.append("■ ", 0.0, egui::TextFormat::simple(font.clone(), *swatch)); - } - job.append(title, 0.0, egui::TextFormat::simple(font.clone(), color)); - job + /// 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() } } @@ -816,36 +793,30 @@ impl DockContent for EffectContent<'_> { /// 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 { - if self.swatches.iter().any(|(p, _)| p == panel) { - return self.tab_job(panel, logical, font, color).into(); - } - self.titles.iter().find(|(p, _)| p == panel).map_or(logical, |(_, s)| s.as_str()).into() + 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() }; + let mut job = egui::text::LayoutJob::default(); + job.append("■ ", 0.0, egui::TextFormat::simple(font.clone(), *swatch)); + job.append(title, 0.0, egui::TextFormat::simple(font.clone(), color)); + job.into() } - fn group_header_width(&self, panels: &[PanelKind], active: &PanelKind, _floating: bool, strip_width: f32) -> f32 { - self.header_width(panels, *active, strip_width) + fn group_header_width(&self, _panels: &[PanelKind], active: &PanelKind, _floating: bool, strip_width: f32) -> f32 { + self.header_width(*active, strip_width) } - /// The Timeline's comp tabs (in its group, whichever panel is shown), then 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) { + /// 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 panels.contains(&PanelKind::Timeline) && !self.docs.is_empty() { - let strip = Rect::from_min_max(header.min, pos2(end.max(header.min.x), header.max.y)); - let selected = *active == PanelKind::Timeline; - self.doc_actions.extend(dock::doc_tabs(ui, strip, &self.docs, selected, &self.style, &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")); @@ -858,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/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index 6ba8bb759..75de44a4a 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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b"; 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> { From 5a16de0948944b3aebc075c071b92f267f680da4 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 03:14:07 +0000 Subject: [PATCH 3/3] Pin craft-libs crates to main 512b5435 --- Cargo.lock | 12 ++++++------ Cargo.toml | 12 ++++++------ crates/pdf/README.md | 2 +- xtask/src/pdf_deps.rs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 644e0fa05..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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" +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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" +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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" +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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" +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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" +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=3723e32ab104bfb3b73fb207dd06a212f8a30e4b#3723e32ab104bfb3b73fb207dd06a212f8a30e4b" +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 b0907c1fb..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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", version = "=0.1.0", default-features = false, features = ["read"] } -craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", 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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", 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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", version = "=0.7.2" } -hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b", 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 1be4b0fe5..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 -`3723e32ab104bfb3b73fb207dd06a212f8a30e4b`, 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/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index 75de44a4a..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 = "3723e32ab104bfb3b73fb207dd06a212f8a30e4b"; +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> {