Merge pull request #1144 from haranishi/fix/1075-panel-focus-outline

fix: パネルのフォーカス枠の欠けを修正 #1075
This commit is contained in:
Brandon Thomas 2026-10-11 18:52:57 -04:00
commit 20f618d378
3 changed files with 68 additions and 15 deletions

View File

@ -581,7 +581,9 @@ pub fn draw_group_chrome(
t: &Tokens,
reg: &mut crate::automation::Registry,
) -> Vec<DockAction> {
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::<bool>(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);

View File

@ -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

View File

@ -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:?}"
);
}
}
}
}
}