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 {
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}