1use 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 {
88 let bg = if open {
89 v.accent
90 } else {
91 v.accent.with_alpha(0.18)
92 };
93 ctx.set_fill_color(bg);
94 ctx.begin_path();
95 ctx.rounded_rect(
96 rect.x + 1.0,
97 rect.y + 2.0,
98 rect.width - 2.0,
99 rect.height - 4.0,
100 4.0,
101 );
102 ctx.fill();
103 }
104 ctx.set_fill_color(if open { Color::white() } else { v.text_color });
105 ctx.fill_text(label, rect.x + 9.0, rect.y + 7.0);
106}
107
108fn paint_panel(ctx: &mut dyn DrawCtx, rect: Rect, style: &MenuStyle) {
109 let v = ctx.visuals();
110 ctx.set_fill_color(Color::black().with_alpha(style.shadow_alpha));
111 ctx.begin_path();
112 ctx.rounded_rect(
113 rect.x + style.shadow_offset.0,
114 rect.y + style.shadow_offset.1,
115 rect.width,
116 rect.height,
117 style.radius,
118 );
119 ctx.fill();
120
121 ctx.set_fill_color(v.panel_fill);
122 ctx.begin_path();
123 ctx.rounded_rect(rect.x, rect.y, rect.width, rect.height, style.radius);
124 ctx.fill();
125 ctx.set_stroke_color(v.widget_stroke);
126 ctx.set_line_width(1.0);
127 ctx.begin_path();
128 ctx.rounded_rect(rect.x, rect.y, rect.width, rect.height, style.radius);
129 ctx.stroke();
130}
131
132fn paint_separator(ctx: &mut dyn DrawCtx, rect: Rect) {
133 let v = ctx.visuals();
134 ctx.set_stroke_color(v.widget_stroke.with_alpha(0.55));
135 ctx.set_line_width(1.0);
136 ctx.begin_path();
137 ctx.move_to(rect.x + 8.0, rect.y + SEP_H * 0.5);
138 ctx.line_to(rect.x + rect.width - 8.0, rect.y + SEP_H * 0.5);
139 ctx.stroke();
140}
141
142fn paint_item_row(
143 ctx: &mut dyn DrawCtx,
144 rect: Rect,
145 item: &super::model::MenuItem,
146 hovered: bool,
147 open: bool,
148 style: &MenuStyle,
149) {
150 let v = ctx.visuals();
151 let hovered = hovered && item.enabled;
152 let open = open && item.enabled;
153 if hovered || open {
156 let bg = if open {
157 v.accent
158 } else {
159 v.accent.with_alpha(0.18)
160 };
161 ctx.set_fill_color(bg);
162 ctx.begin_path();
163 ctx.rounded_rect(
164 rect.x + 3.0,
165 rect.y + 2.0,
166 rect.width - 6.0,
167 rect.height - 4.0,
168 3.0,
169 );
170 ctx.fill();
171 }
172
173 let text_color = if !item.enabled {
174 v.text_color.with_alpha(0.45)
175 } else if open {
176 Color::white()
177 } else {
178 v.text_color
179 };
180 ctx.set_fill_color(text_color);
181 if let Some(icon) = item.icon {
182 let icon = icon.to_string();
183 ctx.fill_text(&icon, rect.x + style.icon_x, rect.y + 7.0);
184 } else {
185 match item.selection {
186 MenuSelection::Check { selected: true } => {
187 ctx.fill_text("\u{f00c}", rect.x + style.icon_x, rect.y + 7.0);
188 }
189 MenuSelection::Radio { selected: true } => {
190 ctx.fill_text("\u{f111}", rect.x + style.icon_x, rect.y + 7.0);
191 }
192 MenuSelection::None
193 | MenuSelection::Check { selected: false }
194 | MenuSelection::Radio { selected: false } => {}
195 }
196 }
197 ctx.fill_text(&item.label, rect.x + style.label_x, rect.y + 7.0);
198 if let Some(shortcut) = &item.shortcut {
199 let width = ctx
200 .measure_text(shortcut)
201 .map(|metrics| metrics.width)
202 .unwrap_or(0.0);
203 ctx.fill_text(
204 shortcut,
205 rect.x + rect.width - style.shortcut_right - width,
206 rect.y + 7.0,
207 );
208 }
209 if item.has_submenu() {
210 ctx.fill_text("\u{f105}", rect.x + rect.width - 18.0, rect.y + 7.0);
211 }
212}
213
214fn items_for_layout<'a>(items: &'a [MenuEntry], path: &[usize]) -> &'a [MenuEntry] {
215 let mut current = items;
216 for &idx in path {
217 let Some(MenuEntry::Item(item)) = current.get(idx) else {
218 return current;
219 };
220 current = &item.submenu;
221 }
222 current
223}