From d91e5f6aea2772899bb314092605cf13beec30ee Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 19 Oct 2026 13:04:52 +0000 Subject: [PATCH] docking: paint panel icons in code with DockContent::paint_icon --- crates/ui/src/docking/iconic_tests.rs | 43 ++++++++++++++++++++++++++ crates/ui/src/docking/render/iconic.rs | 14 +++++---- crates/ui/src/docking/surface.rs | 10 ++++++ docs/ui-docking.md | 3 +- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/crates/ui/src/docking/iconic_tests.rs b/crates/ui/src/docking/iconic_tests.rs index 62a2473b..b9ad4a88 100644 --- a/crates/ui/src/docking/iconic_tests.rs +++ b/crates/ui/src/docking/iconic_tests.rs @@ -324,3 +324,46 @@ fn hit_rects_cover_every_gesture_source() { assert!(opened || !out.actions.is_empty(), "{hit:?} at {rect:?} does nothing"); } } + +/// Paints panel 1's icon as a dot in code; panel 2 has neither a painted icon nor an image. +struct Painted; + +impl DockContent for Painted { + fn body(&mut self, _: &mut egui::Ui, _: &u32) {} + fn logical_label(&self, panel: &u32) -> String { + format!("Panel {panel}") + } + fn paint_icon(&mut self, painter: &egui::Painter, rect: Rect, panel: &u32, color: egui::Color32) -> bool { + if *panel != 1 { + return false; + } + painter.circle_filled(rect.center(), 3.0, color); + true + } +} + +#[test] +fn an_icon_painted_in_code_replaces_the_initial_and_others_fall_back() { + let d = DockDriver::new(docked(&[&[1, 2]], true), Painted); + let (one, two) = (d.hit(&Hit::Icon(1)).unwrap(), d.hit(&Hit::Icon(2)).unwrap()); + let dots: Vec = d + .shapes() + .iter() + .filter_map(|clipped| match &clipped.shape { + egui::Shape::Circle(circle) if circle.radius == 3.0 => Some(circle.center), + _ => None, + }) + .collect(); + assert_eq!(dots.len(), 1, "{dots:?}"); + assert!(one.contains(dots[0]) && !two.contains(dots[0]), "the dot sits in panel 1's cell: {dots:?}"); + let initials: Vec = d + .shapes() + .iter() + .filter_map(|clipped| match &clipped.shape { + egui::Shape::Text(text) if text.galley.text() == "P" => Some(text.pos), + _ => None, + }) + .collect(); + assert_eq!(initials.len(), 1, "only panel 2 draws its initial: {initials:?}"); + assert!(two.contains(initials[0])); +} diff --git a/crates/ui/src/docking/render/iconic.rs b/crates/ui/src/docking/render/iconic.rs index 79a37f27..7b908b4a 100644 --- a/crates/ui/src/docking/render/iconic.rs +++ b/crates/ui/src/docking/render/iconic.rs @@ -230,12 +230,14 @@ impl Renderer<'_, P> { let inset = ((self.style.iconic_width - size) * 0.5).max(2.0); let icon = Rect::from_min_size(pos2(cell.left() + inset, cell.center().y - size * 0.5), vec2(size, size)); let color = if current { self.style.text } else { self.style.inactive_text }; - if let Some(source) = self.content.icon(panel) { - egui::Image::new(source).tint(color).paint_at(ui, icon.intersect(cell)); - } else { - let initial: String = label.chars().next().into_iter().flat_map(char::to_uppercase).collect(); - painter.rect_stroke(icon, 3.0, Stroke::new(1.0, color), StrokeKind::Inside); - painter.text(icon.center(), egui::Align2::CENTER_CENTER, initial, self.style.font.clone(), color); + if !self.content.paint_icon(&painter, icon, panel, color) { + if let Some(source) = self.content.icon(panel) { + egui::Image::new(source).tint(color).paint_at(ui, icon.intersect(cell)); + } else { + let initial: String = label.chars().next().into_iter().flat_map(char::to_uppercase).collect(); + painter.rect_stroke(icon, 3.0, Stroke::new(1.0, color), StrokeKind::Inside); + painter.text(icon.center(), egui::Align2::CENTER_CENTER, initial, self.style.font.clone(), color); + } } if labels { let text = self.content.label(ui, panel, &label, &self.style.font, color); diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 66c0ad8c..619a87cf 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -371,9 +371,16 @@ pub trait DockContent

{ fn floating_size(&self, _panel: &P) -> Option { None } + /// An image for the panel's icon, tinted with the icon color. [`Self::paint_icon`] goes first. fn icon(&self, _panel: &P) -> Option> { None } + /// Paint the panel's icon in code into `rect` with `color`, and return true. False draws + /// [`Self::icon`], or the label's initial when that is `None`. `painter` is clipped to the + /// icon's cell. + fn paint_icon(&mut self, _painter: &egui::Painter, _rect: Rect, _panel: &P, _color: Color32) -> bool { + false + } /// Whether the panel may move to a native OS window (feature `native-windows`). fn allow_native(&self, _panel: &P) -> bool { false @@ -455,6 +462,9 @@ impl + ?Sized> DockContent

for Overrides<'_, P, C> { fn icon(&self, panel: &P) -> Option> { self.inner.icon(panel) } + fn paint_icon(&mut self, painter: &egui::Painter, rect: Rect, panel: &P, color: Color32) -> bool { + self.inner.paint_icon(painter, rect, panel, color) + } fn allow_native(&self, panel: &P) -> bool { self.inner.allow_native(panel) } diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 35c8612a..0434d20d 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -104,7 +104,8 @@ its own command path, so pointer, keyboard and control commands share one path. `DockContent

` is the app's adapter. It draws each panel body and gives its label, icon, permissions, size limits, preferred floating size, panel menu and group header. `bare` hides the -tab strip of a lone panel. Panel bodies keep a stable egui id when they move, so widget memory +tab strip of a lone panel. An icon is an image (`icon`), or drawn in code by `paint_icon`, which +gets a painter, the icon rect and the tint color and returns true when it painted. Panel bodies keep a stable egui id when they move, so widget memory survives. `DockOutput` also has: