1use std::borrow::Cow;
6use std::collections::HashMap;
7use std::rc::Rc;
8
9use iced::advanced::widget::text::Style as TextStyle;
10use iced_widget::core::{Element, renderer};
11
12use crate::iced_core::{Alignment, Length};
13use crate::widget::menu::action::MenuAction;
14use crate::widget::menu::key_bind::KeyBind;
15use crate::widget::{Button, RcElementWrapper, icon};
16use crate::{theme, widget};
17
18#[allow(missing_debug_implementations)]
27#[derive(Clone)]
28pub struct MenuTree<Message> {
29 pub(crate) index: usize,
32
33 pub(crate) item: RcElementWrapper<Message>,
35 pub(crate) children: Vec<MenuTree<Message>>,
37 pub(crate) width: Option<u16>,
39 pub(crate) height: Option<u16>,
41}
42
43impl<Message: Clone + 'static> MenuTree<Message> {
44 pub fn new(item: impl Into<RcElementWrapper<Message>>) -> Self {
46 Self {
47 index: 0,
48 item: item.into(),
49 children: Vec::new(),
50 width: None,
51 height: None,
52 }
53 }
54
55 pub fn with_children(
57 item: impl Into<RcElementWrapper<Message>>,
58 children: Vec<impl Into<MenuTree<Message>>>,
59 ) -> Self {
60 Self {
61 index: 0,
62 item: item.into(),
63 children: children.into_iter().map(Into::into).collect(),
64 width: None,
65 height: None,
66 }
67 }
68
69 #[must_use]
74 pub fn width(mut self, width: u16) -> Self {
75 self.width = Some(width);
76 self
77 }
78
79 #[must_use]
84 pub fn height(mut self, height: u16) -> Self {
85 self.height = Some(height);
86 self
87 }
88
89 pub(crate) fn set_index(&mut self) {
93 fn rec<Message: Clone + 'static>(mt: &mut MenuTree<Message>, count: &mut usize) {
95 mt.children.iter_mut().for_each(|c| {
97 c.index = *count;
98 *count += 1;
99 });
100
101 mt.children.iter_mut().for_each(|c| rec(c, count));
102 }
103
104 let mut count = 0;
105 self.index = count;
106 count += 1;
107 rec(self, &mut count);
108 }
109
110 pub(crate) fn flattern(&self) -> Vec<&Self> {
112 fn rec<'a, Message: Clone + 'static>(
114 mt: &'a MenuTree<Message>,
115 flat: &mut Vec<&'a MenuTree<Message>>,
116 ) {
117 mt.children.iter().for_each(|c| {
118 flat.push(c);
119 });
120
121 mt.children.iter().for_each(|c| {
122 rec(&c, flat);
123 });
124 }
125
126 let mut flat = Vec::new();
127 flat.push(self);
128 rec(self, &mut flat);
129
130 flat
131 }
132}
133
134impl<Message: Clone + 'static> From<crate::Element<'static, Message>> for MenuTree<Message> {
135 fn from(value: crate::Element<'static, Message>) -> Self {
136 Self::new(RcElementWrapper::new(value))
137 }
138}
139
140pub fn menu_button<'a, Message>(
141 children: Vec<crate::Element<'a, Message>>,
142) -> crate::widget::Button<'a, Message>
143where
144 Message: std::clone::Clone + 'a,
145{
146 widget::button::custom(
147 widget::Row::with_children(children)
148 .align_y(Alignment::Center)
149 .height(Length::Fill)
150 .width(Length::Fill),
151 )
152 .height(Length::Fixed(36.0))
153 .padding([4, 16])
154 .width(Length::Fill)
155 .class(theme::Button::MenuItem)
156}
157
158#[derive(Clone)]
159pub enum MenuItem<A: MenuAction, L: Into<Cow<'static, str>>> {
173 Button(L, Option<icon::Handle>, A),
175 ButtonDisabled(L, Option<icon::Handle>, A),
177 CheckBox(L, Option<icon::Handle>, bool, A),
179 Folder(L, Vec<MenuItem<A, L>>),
181 Divider,
183}
184
185pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
193 label: impl Into<Cow<'a, str>> + 'a,
194) -> Button<'a, Message>
195where
196 Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
197 Message: std::clone::Clone + 'a,
198{
199 widget::button::custom(widget::text(label))
200 .padding([4, 12])
201 .class(theme::Button::MenuRoot)
202}
203
204#[must_use]
215pub fn menu_items<
216 A: MenuAction<Message = Message>,
217 L: Into<Cow<'static, str>> + 'static,
218 Message: 'static + std::clone::Clone,
219>(
220 key_binds: &HashMap<KeyBind, A>,
221 children: Vec<MenuItem<A, L>>,
222) -> Vec<MenuTree<Message>> {
223 fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
224 for (key_bind, key_action) in key_binds {
225 if action == key_action {
226 return key_bind.to_string();
227 }
228 }
229 String::new()
230 }
231
232 fn key_style(theme: &crate::Theme) -> TextStyle {
233 let mut color = theme.cosmic().background.component.on;
234 color.alpha *= 0.75;
235 TextStyle {
236 color: Some(color.into()),
237 }
238 }
239 let key_class = theme::Text::Custom(key_style);
240
241 let size = children.len();
242
243 children
244 .into_iter()
245 .enumerate()
246 .flat_map(|(i, item)| {
247 let mut trees = vec![];
248 let spacing = crate::theme::spacing();
249
250 match item {
251 MenuItem::Button(label, icon, action) => {
252 let l: Cow<'static, str> = label.into();
253 let key = find_key(&action, key_binds);
254 let mut items = vec![
255 widget::text(l.clone()).into(),
256 widget::horizontal_space().into(),
257 widget::text(key).class(key_class).into(),
258 ];
259
260 if let Some(icon) = icon {
261 items.insert(0, widget::icon::icon(icon).size(14).into());
262 items.insert(1, widget::Space::with_width(spacing.space_xxs).into());
263 }
264
265 let menu_button = menu_button(items).on_press(action.message());
266
267 trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
268 }
269 MenuItem::ButtonDisabled(label, icon, action) => {
270 let l: Cow<'static, str> = label.into();
271
272 let key = find_key(&action, key_binds);
273
274 let mut items = vec![
275 widget::text(l.clone()).into(),
276 widget::horizontal_space().into(),
277 widget::text(key).class(key_class).into(),
278 ];
279
280 if let Some(icon) = icon {
281 items.insert(0, widget::icon::icon(icon).size(14).into());
282 items.insert(1, widget::Space::with_width(spacing.space_xxs).into());
283 }
284
285 let menu_button = menu_button(items);
286
287 trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
288 }
289 MenuItem::CheckBox(label, icon, value, action) => {
290 let key = find_key(&action, key_binds);
291 let mut items = vec![
292 if value {
293 widget::icon::from_name("object-select-symbolic")
294 .size(16)
295 .icon()
296 .class(theme::Svg::Custom(Rc::new(|theme| {
297 iced_widget::svg::Style {
298 color: Some(theme.cosmic().accent_text_color().into()),
299 }
300 })))
301 .width(Length::Fixed(16.0))
302 .into()
303 } else {
304 widget::Space::with_width(Length::Fixed(16.0)).into()
305 },
306 widget::Space::with_width(spacing.space_xxs).into(),
307 widget::text(label).align_x(iced::Alignment::Start).into(),
308 widget::horizontal_space().into(),
309 widget::text(key).class(key_class).into(),
310 ];
311
312 if let Some(icon) = icon {
313 items.insert(1, widget::Space::with_width(spacing.space_xxs).into());
314 items.insert(2, widget::icon::icon(icon).size(14).into());
315 }
316
317 trees.push(MenuTree::from(Element::from(
318 menu_button(items).on_press(action.message()),
319 )));
320 }
321 MenuItem::Folder(label, children) => {
322 let l: Cow<'static, str> = label.into();
323
324 trees.push(MenuTree::<Message>::with_children(
325 RcElementWrapper::new(crate::Element::from(
326 menu_button::<'static, _>(vec![
327 widget::text(l.clone()).into(),
328 widget::horizontal_space().into(),
329 widget::icon::from_name("pan-end-symbolic")
330 .size(16)
331 .icon()
332 .into(),
333 ])
334 .class(
335 if children.is_empty() {
337 theme::Button::MenuItem
339 } else {
340 theme::Button::MenuFolder
342 },
343 ),
344 )),
345 menu_items(key_binds, children),
346 ));
347 }
348 MenuItem::Divider => {
349 if i != size - 1 {
350 trees.push(MenuTree::<Message>::from(Element::from(
351 widget::divider::horizontal::light(),
352 )));
353 }
354 }
355 }
356 trees
357 })
358 .collect()
359}