agg_gui/widgets/window/chrome.rs
1//! Reusable paint helpers for window-style chrome — shadow halo,
2//! rounded body fill, title-bar fill, collapse chevron, outer border.
3//!
4//! Why this module exists: the [`Window`](super::Window) widget owns a
5//! lot of behaviour (drag, resize, maximize, close, snap, backbuffer)
6//! that other "framed" UI elements don't want. AtomArtist's node
7//! editor — and any other consumer that wants the same drop-shadow +
8//! rounded-corner look — can call these stateless paint functions
9//! directly without inheriting the rest of Window.
10//!
11//! All coords are local to the framed region (origin at bottom-left,
12//! Y-up — agg-gui convention). The caller is responsible for
13//! translating the `DrawCtx` into the frame's local space before
14//! invoking these helpers.
15
16use crate::color::Color;
17use crate::draw_ctx::DrawCtx;
18use crate::theme::Visuals;
19
20/// Geometry + colour bundle for a window-style frame. Construct one
21/// per paint pass; default presets come from [`ChromeStyle::from_visuals`]
22/// (matches the live `Window` widget exactly).
23#[derive(Clone, Debug)]
24pub struct ChromeStyle {
25 pub corner_radius: f64,
26 pub title_height: f64,
27
28 pub shadow_blur: f64,
29 pub shadow_dx: f64,
30 pub shadow_dy: f64,
31 pub shadow_steps: usize,
32 pub shadow_color: Color,
33
34 pub body_color: Color,
35 pub border_color: Color,
36 pub title_color: Color,
37 pub title_text_color: Color,
38 pub separator_color: Color,
39}
40
41impl ChromeStyle {
42 /// Mirrors the constants `Window` paints with, so a refactored
43 /// `Window` and a fresh consumer (NodeWidget) produce identical
44 /// pixels for the same visuals snapshot.
45 pub fn from_visuals(v: &Visuals) -> Self {
46 Self {
47 corner_radius: 8.0,
48 title_height: 28.0,
49 shadow_blur: 14.0,
50 shadow_dx: 2.0,
51 shadow_dy: 6.0,
52 shadow_steps: 10,
53 shadow_color: v.window_shadow,
54 body_color: v.window_fill,
55 border_color: v.window_stroke,
56 title_color: v.window_title_fill,
57 title_text_color: v.window_title_text,
58 separator_color: v.window_stroke,
59 }
60 }
61}
62
63/// Paint the stacked-rounded-rect drop shadow that frames a window.
64/// Drawn outside-in so the denser core overlays the softer halo.
65pub fn paint_chrome_shadow(ctx: &mut dyn DrawCtx, w: f64, h: f64, style: &ChromeStyle) {
66 let base = style.shadow_color;
67 let steps = style.shadow_steps.max(1);
68 for i in (0..steps).rev() {
69 let t = i as f64 / steps as f64;
70 let infl = t * style.shadow_blur;
71 let falloff = (1.0 - t).powi(2) as f32;
72 let alpha = base.a * falloff / steps as f32 * 6.0;
73 ctx.set_fill_color(Color::rgba(base.r, base.g, base.b, alpha));
74 ctx.begin_path();
75 ctx.rounded_rect(
76 style.shadow_dx - infl,
77 -style.shadow_dy - infl,
78 w + 2.0 * infl,
79 h + 2.0 * infl,
80 style.corner_radius + infl,
81 );
82 ctx.fill();
83 }
84}
85
86/// Paint the rounded body fill. When `collapsed` is true the body
87/// occupies the full `h` with all four corners rounded; otherwise the
88/// title-bar strip at the top is excluded (the caller paints that
89/// separately so the top corner radius reads as one shape, no
90/// overlapping fills).
91pub fn paint_chrome_body(
92 ctx: &mut dyn DrawCtx,
93 w: f64,
94 h: f64,
95 style: &ChromeStyle,
96 collapsed: bool,
97) {
98 if collapsed {
99 return;
100 }
101 let content_h = (h - style.title_height).max(0.0);
102 if content_h <= 0.0 {
103 return;
104 }
105 let r = style.corner_radius;
106 ctx.set_fill_color(style.body_color);
107 ctx.begin_path();
108 ctx.rounded_rect(0.0, 0.0, w, content_h, r);
109 ctx.rect(0.0, (content_h - r).max(0.0), w, r.min(content_h));
110 ctx.fill();
111}
112
113/// Paint the title-bar fill + 1-px bottom separator + title label.
114/// Does **not** paint the chevron or any buttons — those are real
115/// child widgets ([`crate::widgets::ChevronWidget`] et al.) the caller
116/// composes into the title bar's child list. This keeps interaction
117/// (click + hover + focus) flowing through the standard parent/child
118/// event dispatch instead of manual coordinate hit-tests.
119///
120/// `bar_x`/`bar_y` are the bar's lower-left in the frame's local
121/// coordinate space; the bar's width is the frame's width and its
122/// height is `style.title_height`.
123pub fn paint_chrome_title_bar(
124 ctx: &mut dyn DrawCtx,
125 bar_x: f64,
126 bar_y: f64,
127 w: f64,
128 style: &ChromeStyle,
129 collapsed: bool,
130 title: &str,
131 font_size: f64,
132) {
133 let r = style.corner_radius;
134 let h = style.title_height;
135
136 // Fill — expanded windows pair with a square bottom edge against
137 // the body separator; collapsed windows carry all four corners.
138 ctx.set_fill_color(style.title_color);
139 ctx.begin_path();
140 ctx.rounded_rect(bar_x, bar_y, w, h, r);
141 if !collapsed {
142 // Square the bottom edge by overpainting the lower r-strip.
143 ctx.rect(bar_x, bar_y, w, r.min(h));
144 }
145 ctx.fill();
146
147 // 1-px separator at bar's bottom edge (only when expanded).
148 if !collapsed {
149 ctx.set_fill_color(style.separator_color);
150 ctx.begin_path();
151 ctx.rect(bar_x, bar_y, w, 1.0);
152 ctx.fill();
153 }
154
155 // Title label. Inset to clear the chevron child slot on the left
156 // (chevron occupies ~24 px when composed by the caller).
157 if !title.is_empty() {
158 ctx.set_fill_color(style.title_text_color);
159 ctx.set_font_size(font_size);
160 ctx.fill_text(title, bar_x + 24.0, bar_y + h * 0.5 - 4.0);
161 }
162}
163
164/// Stroked outer border that frames body + title together.
165pub fn paint_chrome_border(ctx: &mut dyn DrawCtx, w: f64, h: f64, style: &ChromeStyle) {
166 ctx.set_stroke_color(style.border_color);
167 ctx.set_line_width(1.0);
168 ctx.begin_path();
169 ctx.rounded_rect(
170 0.5,
171 0.5,
172 (w - 1.0).max(0.0),
173 (h - 1.0).max(0.0),
174 style.corner_radius,
175 );
176 ctx.stroke();
177}
178
179/// Paint the collapse / expand chevron at `(cx, cy)` in the active
180/// `DrawCtx` space. Half-size 4 px. Iconography matches conventional
181/// UIs: ▸ when collapsed (click to expand), ▾ when expanded (click to
182/// collapse). agg-gui is Y-up, so "▾" has its apex at the LOWER y.
183pub fn paint_chevron(ctx: &mut dyn DrawCtx, cx: f64, cy: f64, collapsed: bool, color: Color) {
184 let sz = 4.0;
185 ctx.set_stroke_color(color);
186 ctx.set_line_width(1.5);
187 ctx.begin_path();
188 if collapsed {
189 // ▸ pointing right.
190 ctx.move_to(cx, cy - sz);
191 ctx.line_to(cx + sz, cy);
192 ctx.line_to(cx, cy + sz);
193 } else {
194 // ▾ pointing down — apex at the lower y in Y-up coords.
195 ctx.move_to(cx - sz, cy + sz * 0.5);
196 ctx.line_to(cx, cy - sz * 0.5);
197 ctx.line_to(cx + sz, cy + sz * 0.5);
198 }
199 ctx.stroke();
200}