mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 20:16:52 -06:00
Merge pull request #2458 from aiongg/comp-tabs
Timeline: each open comp is a tab in the group's tab row, with an overflow menu
This commit is contained in:
Generated
+6
-6
@@ -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",
|
||||
|
||||
+6
-6
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
+103
-43
@@ -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.<comp>`, 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<DocAction> {
|
||||
/// 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<f32> {
|
||||
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.<comp>`, `panel.tab.Timeline.close`
|
||||
/// for the shown comp's ×, `panel.tab.Timeline.more` for », and `panel.tab.Timeline.more.<comp>`
|
||||
/// 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<DocAction> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<DocTab>,
|
||||
/// 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<DocAction>,
|
||||
/// Viewer locks clicked.
|
||||
locks: Vec<PanelKind>,
|
||||
@@ -750,8 +751,12 @@ fn window_bodies(window: &Floating<PanelKind>) -> 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<PanelKind> 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<PanelKind> 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<PanelKind> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,3 +424,45 @@ fn a_tab_dropped_on_a_frame_joins_it() {
|
||||
fn group_of(w: &Workspace, p: PanelKind) -> Option<Vec<PanelKind>> {
|
||||
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<u64> = 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<u64> = 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}")));
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user