agg_gui/widgets/menu/paint.rs
1//! Themed painting for popup menus and menu bars.
2//!
3//! Text rendering is composed: the bar and popup widgets own `Label` widgets
4//! for every text-bearing element (bar button text, item labels, shortcut
5//! strings). This module paints the chrome (backgrounds, hover panels,
6//! separators, submenu chevrons, check/radio glyphs) inline, and the bar /
7//! popup widgets paint their owned `Label` children through `paint_subtree`
8//! so the framework's backbuffer + LCD subpixel path renders every glyph.
9
10use crate::color::Color;
11use crate::draw_ctx::DrawCtx;
12use crate::geometry::Rect;
13
14use super::geometry::SEP_H;
15
16/// Style values shared between the bar and popup painters.
17///
18/// Geometry only — every chrome indicator (submenu chevron, check
19/// mark, radio dot) is painted as vector primitives by the menu
20/// widget, so the menu renders the same on every host regardless of
21/// which icon font (if any) it bundles.
22#[derive(Clone)]
23pub struct MenuStyle {
24 pub radius: f64,
25 pub shadow_offset: (f64, f64),
26 pub shadow_alpha: f32,
27 pub pad_x: f64,
28 pub icon_x: f64,
29 pub label_x: f64,
30 pub shortcut_right: f64,
31}
32
33impl Default for MenuStyle {
34 fn default() -> Self {
35 Self {
36 radius: 5.0,
37 shadow_offset: (5.0, -5.0),
38 shadow_alpha: 0.22,
39 pad_x: 8.0,
40 icon_x: 14.0,
41 label_x: 32.0,
42 shortcut_right: 28.0,
43 }
44 }
45}
46
47/// Three points (apex on the right, top-left, bottom-left) of the
48/// submenu-indicator chevron painted at the right edge of a popup row.
49///
50/// Returned as a pure function so paint tests can verify the geometry
51/// without driving a `DrawCtx`. Coordinates are in the same Y-up space
52/// the row uses; the apex points to the right (toward where the
53/// submenu opens).
54pub fn submenu_chevron_points(row: Rect) -> [(f64, f64); 3] {
55 let cx = row.x + row.width - 11.0;
56 let cy = row.y + row.height * 0.5;
57 let half_h = 4.0;
58 let arm = 3.0;
59 [(cx - arm, cy + half_h), (cx, cy), (cx - arm, cy - half_h)]
60}
61
62/// Paint a check-mark indicator centred at `(cx, cy)` as a stroked
63/// polyline. Font-independent — used for both check and radio rows
64/// so the selection marker is identical regardless of the host's
65/// bundled icon font.
66pub fn paint_check_mark(ctx: &mut dyn DrawCtx, cx: f64, cy: f64, color: Color) {
67 // Three-point ✓ sized to fit comfortably in the menu's left icon
68 // slot (~12 px tall). Stroke width matches the chevron so the
69 // visual weight is consistent.
70 ctx.set_stroke_color(color);
71 ctx.set_line_width(1.6);
72 ctx.begin_path();
73 ctx.move_to(cx - 5.0, cy - 0.5);
74 ctx.line_to(cx - 1.5, cy - 4.0);
75 ctx.line_to(cx + 5.0, cy + 3.5);
76 ctx.stroke();
77}
78
79/// Paint the submenu-indicator chevron as a stroked `>` polyline.
80/// Font-independent — the previous implementation rendered a glyph
81/// from the popup's text font, which left a tofu box on hosts whose
82/// font (or icon fallback) lacked the configured code point.
83pub fn paint_submenu_chevron(ctx: &mut dyn DrawCtx, row: Rect, color: Color) {
84 let [a, b, c] = submenu_chevron_points(row);
85 ctx.set_stroke_color(color);
86 ctx.set_line_width(1.4);
87 ctx.begin_path();
88 ctx.move_to(a.0, a.1);
89 ctx.line_to(b.0, b.1);
90 ctx.line_to(c.0, c.1);
91 ctx.stroke();
92}
93
94/// Paint the chrome (hover / open background fill) under a bar button.
95/// The button's text label is painted separately by the caller via
96/// `paint_subtree` on the bar's owned `Label`.
97pub fn paint_menu_bar_button_bg(ctx: &mut dyn DrawCtx, rect: Rect, open: bool, hovered: bool) {
98 if !open && !hovered {
99 return;
100 }
101 let v = ctx.visuals();
102 // Subtle desktop hover: a translucent accent tint under the label,
103 // not the full accent. Translucent because the underlying
104 // `top_bar_bg` is already a very light gray (≈0.88 in the light
105 // theme); a fully-opaque `widget_bg_hovered` panel reads as nothing
106 // — `widget_bg_hovered` is only ~0.04 brighter than the bar.
107 // Translucent accent stays visible on either theme while reserving
108 // the FULL accent for the OPENED state.
109 let bg = if open {
110 v.accent
111 } else {
112 v.accent.with_alpha(0.18)
113 };
114 ctx.set_fill_color(bg);
115 ctx.begin_path();
116 ctx.rounded_rect(
117 rect.x + 1.0,
118 rect.y + 2.0,
119 rect.width - 2.0,
120 rect.height - 4.0,
121 4.0,
122 );
123 ctx.fill();
124}
125
126/// The text colour the bar button's `Label` should use for the given
127/// open / enabled state. Returns `Color::white()` when the button is
128/// open (white text on the accent fill), the theme's `text_color`
129/// otherwise.
130pub fn bar_button_text_color(ctx: &dyn DrawCtx, open: bool) -> Color {
131 if open {
132 Color::white()
133 } else {
134 ctx.visuals().text_color
135 }
136}
137
138/// The text colour the popup row's `Label` should use for the given
139/// open / hovered / enabled state. Mirrors the historical
140/// `paint_item_row` logic but exposed so the popup widget can push the
141/// resolved colour into its owned `Label`s.
142pub fn popup_row_text_color(ctx: &dyn DrawCtx, enabled: bool, open: bool) -> Color {
143 let v = ctx.visuals();
144 if !enabled {
145 v.text_color.with_alpha(0.45)
146 } else if open {
147 Color::white()
148 } else {
149 v.text_color
150 }
151}
152
153pub fn paint_panel(ctx: &mut dyn DrawCtx, rect: Rect, style: &MenuStyle) {
154 let v = ctx.visuals();
155 ctx.set_fill_color(Color::black().with_alpha(style.shadow_alpha));
156 ctx.begin_path();
157 ctx.rounded_rect(
158 rect.x + style.shadow_offset.0,
159 rect.y + style.shadow_offset.1,
160 rect.width,
161 rect.height,
162 style.radius,
163 );
164 ctx.fill();
165
166 ctx.set_fill_color(v.panel_fill);
167 ctx.begin_path();
168 ctx.rounded_rect(rect.x, rect.y, rect.width, rect.height, style.radius);
169 ctx.fill();
170 ctx.set_stroke_color(v.widget_stroke);
171 ctx.set_line_width(1.0);
172 ctx.begin_path();
173 ctx.rounded_rect(rect.x, rect.y, rect.width, rect.height, style.radius);
174 ctx.stroke();
175}
176
177pub fn paint_separator(ctx: &mut dyn DrawCtx, rect: Rect) {
178 let v = ctx.visuals();
179 ctx.set_stroke_color(v.widget_stroke.with_alpha(0.55));
180 ctx.set_line_width(1.0);
181 ctx.begin_path();
182 ctx.move_to(rect.x + 8.0, rect.y + SEP_H * 0.5);
183 ctx.line_to(rect.x + rect.width - 8.0, rect.y + SEP_H * 0.5);
184 ctx.stroke();
185}
186
187/// Paint the hover / open background of a popup item row. The row's
188/// label text is painted separately via `paint_subtree` on the
189/// popup-owned `Label`; this only fills the rounded backdrop.
190pub fn paint_item_row_bg(
191 ctx: &mut dyn DrawCtx,
192 rect: Rect,
193 hovered: bool,
194 open: bool,
195 enabled: bool,
196) {
197 let hovered = hovered && enabled;
198 let open = open && enabled;
199 if !hovered && !open {
200 return;
201 }
202 // Same subtle/strong split as the bar button: hover hints with a
203 // translucent accent tint; open commits with full accent.
204 let v = ctx.visuals();
205 let bg = if open {
206 v.accent
207 } else {
208 v.accent.with_alpha(0.18)
209 };
210 ctx.set_fill_color(bg);
211 ctx.begin_path();
212 ctx.rounded_rect(
213 rect.x + 3.0,
214 rect.y + 2.0,
215 rect.width - 6.0,
216 rect.height - 4.0,
217 3.0,
218 );
219 ctx.fill();
220}