Skip to main content

agg_gui/widgets/menu/
paint.rs

1//! Themed painting for popup menus and menu bars.
2//!
3//! The painter deliberately sets every fill and stroke it uses so menu colors
4//! never inherit accidental state from the widget that opened the popup.
5
6use std::sync::Arc;
7
8use crate::color::Color;
9use crate::draw_ctx::DrawCtx;
10use crate::geometry::Rect;
11use crate::text::Font;
12
13use super::geometry::{PopupLayout, SEP_H};
14use super::model::{MenuEntry, MenuSelection};
15use super::state::PopupMenuState;
16
17#[derive(Clone)]
18pub struct MenuStyle {
19    pub radius: f64,
20    pub shadow_offset: (f64, f64),
21    pub shadow_alpha: f32,
22    pub pad_x: f64,
23    pub icon_x: f64,
24    pub label_x: f64,
25    pub shortcut_right: f64,
26}
27
28impl Default for MenuStyle {
29    fn default() -> Self {
30        Self {
31            radius: 5.0,
32            shadow_offset: (5.0, -5.0),
33            shadow_alpha: 0.22,
34            pad_x: 8.0,
35            icon_x: 14.0,
36            label_x: 32.0,
37            shortcut_right: 28.0,
38        }
39    }
40}
41
42pub fn paint_popup_stack(
43    ctx: &mut dyn DrawCtx,
44    font: Arc<Font>,
45    font_size: f64,
46    items: &[MenuEntry],
47    state: &PopupMenuState,
48    layouts: &[PopupLayout],
49    style: &MenuStyle,
50) {
51    ctx.set_font(font);
52    ctx.set_font_size(font_size);
53    for layout in layouts {
54        paint_panel(ctx, layout.rect, style);
55        for (entry, row) in items_for_layout(items, &layout.path_prefix)
56            .iter()
57            .zip(&layout.rows)
58        {
59            match entry {
60                MenuEntry::Separator => paint_separator(ctx, row.rect),
61                MenuEntry::Item(item) => {
62                    let mut path = layout.path_prefix.clone();
63                    path.push(row.item_index.unwrap_or_default());
64                    let hovered = state.hover_path.as_ref() == Some(&path);
65                    let open = state.open_path.starts_with(&path);
66                    paint_item_row(ctx, row.rect, item, hovered, open, style);
67                }
68            }
69        }
70    }
71}
72
73pub fn paint_menu_bar_button(
74    ctx: &mut dyn DrawCtx,
75    rect: Rect,
76    label: &str,
77    open: bool,
78    hovered: bool,
79) {
80    let v = ctx.visuals();
81    if open || hovered {
82        ctx.set_fill_color(if open { v.accent } else { v.accent_hovered });
83        ctx.begin_path();
84        ctx.rounded_rect(
85            rect.x + 1.0,
86            rect.y + 2.0,
87            rect.width - 2.0,
88            rect.height - 4.0,
89            4.0,
90        );
91        ctx.fill();
92    }
93    ctx.set_fill_color(if open || hovered {
94        Color::white()
95    } else {
96        v.text_color
97    });
98    ctx.fill_text(label, rect.x + 9.0, rect.y + 7.0);
99}
100
101fn paint_panel(ctx: &mut dyn DrawCtx, rect: Rect, style: &MenuStyle) {
102    let v = ctx.visuals();
103    ctx.set_fill_color(Color::black().with_alpha(style.shadow_alpha));
104    ctx.begin_path();
105    ctx.rounded_rect(
106        rect.x + style.shadow_offset.0,
107        rect.y + style.shadow_offset.1,
108        rect.width,
109        rect.height,
110        style.radius,
111    );
112    ctx.fill();
113
114    ctx.set_fill_color(v.panel_fill);
115    ctx.begin_path();
116    ctx.rounded_rect(rect.x, rect.y, rect.width, rect.height, style.radius);
117    ctx.fill();
118    ctx.set_stroke_color(v.widget_stroke);
119    ctx.set_line_width(1.0);
120    ctx.begin_path();
121    ctx.rounded_rect(rect.x, rect.y, rect.width, rect.height, style.radius);
122    ctx.stroke();
123}
124
125fn paint_separator(ctx: &mut dyn DrawCtx, rect: Rect) {
126    let v = ctx.visuals();
127    ctx.set_stroke_color(v.widget_stroke.with_alpha(0.55));
128    ctx.set_line_width(1.0);
129    ctx.begin_path();
130    ctx.move_to(rect.x + 8.0, rect.y + SEP_H * 0.5);
131    ctx.line_to(rect.x + rect.width - 8.0, rect.y + SEP_H * 0.5);
132    ctx.stroke();
133}
134
135fn paint_item_row(
136    ctx: &mut dyn DrawCtx,
137    rect: Rect,
138    item: &super::model::MenuItem,
139    hovered: bool,
140    open: bool,
141    style: &MenuStyle,
142) {
143    let v = ctx.visuals();
144    let hovered = hovered && item.enabled;
145    let open = open && item.enabled;
146    if hovered || open {
147        ctx.set_fill_color(if open { v.accent } else { v.accent_hovered });
148        ctx.begin_path();
149        ctx.rounded_rect(
150            rect.x + 3.0,
151            rect.y + 2.0,
152            rect.width - 6.0,
153            rect.height - 4.0,
154            3.0,
155        );
156        ctx.fill();
157    }
158
159    let text_color = if !item.enabled {
160        v.text_color.with_alpha(0.45)
161    } else if open || hovered {
162        Color::white()
163    } else {
164        v.text_color
165    };
166    ctx.set_fill_color(text_color);
167    if let Some(icon) = item.icon {
168        let icon = icon.to_string();
169        ctx.fill_text(&icon, rect.x + style.icon_x, rect.y + 7.0);
170    } else {
171        match item.selection {
172            MenuSelection::Check { selected: true } => {
173                ctx.fill_text("\u{f00c}", rect.x + style.icon_x, rect.y + 7.0);
174            }
175            MenuSelection::Radio { selected: true } => {
176                ctx.fill_text("\u{f111}", rect.x + style.icon_x, rect.y + 7.0);
177            }
178            MenuSelection::None
179            | MenuSelection::Check { selected: false }
180            | MenuSelection::Radio { selected: false } => {}
181        }
182    }
183    ctx.fill_text(&item.label, rect.x + style.label_x, rect.y + 7.0);
184    if let Some(shortcut) = &item.shortcut {
185        let width = ctx
186            .measure_text(shortcut)
187            .map(|metrics| metrics.width)
188            .unwrap_or(0.0);
189        ctx.fill_text(
190            shortcut,
191            rect.x + rect.width - style.shortcut_right - width,
192            rect.y + 7.0,
193        );
194    }
195    if item.has_submenu() {
196        ctx.fill_text("\u{f105}", rect.x + rect.width - 18.0, rect.y + 7.0);
197    }
198}
199
200fn items_for_layout<'a>(items: &'a [MenuEntry], path: &[usize]) -> &'a [MenuEntry] {
201    let mut current = items;
202    for &idx in path {
203        let Some(MenuEntry::Item(item)) = current.get(idx) else {
204            return current;
205        };
206        current = &item.submenu;
207    }
208    current
209}