Skip to main content

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}