diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index bd6e73a2..5ccff4e6 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -581,7 +581,9 @@ pub fn draw_group_chrome( t: &Tokens, reg: &mut crate::automation::Registry, ) -> Vec { - draw_group_chrome_details(ui, g, focused, seqs, t, reg).actions + let chrome = draw_group_chrome_details(ui, g, seqs, t, reg); + paint_group_focus(ui, g, focused, t); + chrome.actions } #[derive(Default)] @@ -590,14 +592,7 @@ pub struct Chrome { pub tabs: Vec<(PanelKind, Rect)>, } -pub fn draw_group_chrome_details( - ui: &mut egui::Ui, - g: &Group, - focused: PanelKind, - seqs: &SeqTabs, - t: &Tokens, - reg: &mut crate::automation::Registry, -) -> Chrome { +pub fn draw_group_chrome_details(ui: &mut egui::Ui, g: &Group, seqs: &SeqTabs, t: &Tokens, reg: &mut crate::automation::Registry) -> Chrome { let mut actions = Vec::new(); let mut tab_rects: Vec<(PanelKind, Rect)> = Vec::new(); let painter = ui.painter().clone(); @@ -852,10 +847,6 @@ pub fn draw_group_chrome_details( ui.ctx().data_mut(|d| d.remove::(list_id)); } } - // focus outline - if active_panel == Some(focused) { - painter.rect_stroke(g.rect, 0.0, Stroke::new(1.0, t.focus), StrokeKind::Inside); - } // clicking anywhere in the panel focuses it if let Some(p) = active_panel && ui.rect_contains_pointer(g.rect) @@ -866,6 +857,13 @@ pub fn draw_group_chrome_details( Chrome { actions, tabs: tab_rects } } +/// Paint once after the group's contents so child backgrounds cannot hide its focus (#1075). +pub(crate) fn paint_group_focus(ui: &egui::Ui, group: &Group, focused: PanelKind, tokens: &Tokens) { + if group.panels.get(group.active).copied() == Some(focused) { + ui.painter().rect_stroke(group.rect, 0.0, Stroke::new(1.0, tokens.focus), StrokeKind::Inside); + } +} + fn paint_widget_focus(painter: &egui::Painter, response: &egui::Response, tokens: &Tokens) { if response.has_focus() { painter.rect_stroke(response.rect.shrink(2.0), 2.0, Stroke::new(1.5, tokens.focus), StrokeKind::Inside); diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index d90888ab..4849c08d 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -191,7 +191,7 @@ impl FilmcraftApp { let mut actions = vec![]; let mut targets = vec![]; for group in &groups { - let chrome = dock::draw_group_chrome_details(ui, group, self.ui.focused, &seqs, &t, &mut self.auto); + let chrome = dock::draw_group_chrome_details(ui, group, &seqs, &t, &mut self.auto); if let Some(target) = target(group, chrome.tabs, true) { targets.push(target); } @@ -205,6 +205,9 @@ impl FilmcraftApp { self.panel_body(ui, panel, group.content); } } + for group in &groups { + dock::paint_group_focus(ui, group, self.ui.focused, &t); + } let floating_order = self.floating_order(&ctx); for index in floating_order.into_iter().filter(|_| maximized.is_none()) { if let Some(target) = self.floating_group(&ctx, index, &seqs, &mut actions) { @@ -279,7 +282,7 @@ impl FilmcraftApp { panels: floating.panels.clone(), active: floating.active, }; - let chrome = dock::draw_group_chrome_details(ui, &group, self.ui.focused, seqs, &t, &mut self.auto); + let chrome = dock::draw_group_chrome_details(ui, &group, seqs, &t, &mut self.auto); actions.extend(chrome.actions); if let Some(active) = group.panels.get(group.active).copied() { self.panel_body(ui, active, group.content); @@ -307,6 +310,7 @@ impl FilmcraftApp { if next != rect { let _ = self.dock_command(ctx, Action::MoveFloating { panel, rect: [next.min.x, next.min.y, next.width(), next.height()] }); } + dock::paint_group_focus(ui, &group, self.ui.focused, &t); output = target(&group, chrome.tabs, false); }); output diff --git a/crates/ui-egui/tests/panels_ui.rs b/crates/ui-egui/tests/panels_ui.rs index ed2cf458..504f4f3e 100644 --- a/crates/ui-egui/tests/panels_ui.rs +++ b/crates/ui-egui/tests/panels_ui.rs @@ -520,3 +520,54 @@ fn scopes_of_colour_bars() { assert!(angles.iter().any(|x| (x - a).abs() < 1.5), "{} at {a:.1}° not in {angles:?}", t.name); } } + +/// #1075: child backgrounds must not cover the focus outline shared with Program. Check the +/// rendered primitives, including Media Browser's parent group, in both requested themes. +#[test] +fn focused_panel_outline_is_not_covered_by_child_backgrounds() { + fn rectangles<'a>(shape: &'a egui::Shape, clip: egui::Rect, out: &mut Vec<(&'a egui::epaint::RectShape, egui::Rect)>) { + match shape { + egui::Shape::Rect(rect) => out.push((rect, clip)), + egui::Shape::Vec(shapes) => shapes.iter().for_each(|shape| rectangles(shape, clip, out)), + _ => {} + } + } + let mut d = Driver::demo(); + for theme in ["dark", "light"] { + d.ok("ui.set", json!({"theme": theme})); + for panel in ["Program", "Text", "LumetriScopes", "EssentialSound", "Timeline", "TextToSpeech", "MediaBrowser"] { + d.ok("ui.panel.show", json!({"panel": panel})); + d.ok("ui.set", json!({"focused": panel})); + d.frames(3); + d.snapshot(&format!("focus-{theme}-{panel}"), None); + let mut rects = Vec::new(); + for shape in &d.harness.output().shapes { + rectangles(&shape.shape, shape.clip_rect, &mut rects); + } + let focus = d.harness.state().tokens.focus; + let outlines: Vec<_> = rects + .iter() + .enumerate() + .filter(|(_, (r, _))| r.stroke.color == focus && r.stroke.width == 1.0 && r.rect.width() > 100.0 && r.rect.height() > 80.0) + .collect(); + assert_eq!(outlines.len(), 1, "{theme} {panel}: one focus outline for the whole parent group"); + let (at, (outline, _)) = outlines[0]; + let bounds = outline.rect; + let strips = [ + egui::Rect::from_min_max(bounds.min, egui::pos2(bounds.max.x, bounds.min.y + 1.0)), + egui::Rect::from_min_max(egui::pos2(bounds.min.x, bounds.max.y - 1.0), bounds.max), + egui::Rect::from_min_max(bounds.min, egui::pos2(bounds.min.x + 1.0, bounds.max.y)), + egui::Rect::from_min_max(egui::pos2(bounds.max.x - 1.0, bounds.min.y), bounds.max), + ]; + for (child, clip) in &rects[at + 1..] { + if child.fill.a() != 0 { + let painted = child.rect.intersect(*clip); + assert!( + strips.iter().all(|edge| !edge.intersect(painted).is_positive()), + "{theme} {panel}: a later child background {painted:?} covers the focus outline {bounds:?}" + ); + } + } + } + } +}