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::widget::menu::action::MenuAction;
13use crate::widget::menu::key_bind::KeyBind;
14use crate::widget::{Button, RcElementWrapper, icon};
15use crate::{theme, widget};
16use iced_core::{Alignment, Length};
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::from_vec(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 ..Default::default()
238 }
239 }
240 let key_class = theme::Text::Custom(key_style);
241
242 let size = children.len();
243
244 children
245 .into_iter()
246 .enumerate()
247 .flat_map(|(i, item)| {
248 let mut trees = vec![];
249 let spacing = crate::theme::spacing();
250
251 match item {
252 MenuItem::Button(label, icon, action) => {
253 let l: Cow<'static, str> = label.into();
254 let key = find_key(&action, key_binds);
255 let mut items = vec![
256 widget::text(l)
257 .ellipsize(iced_core::text::Ellipsize::Middle(
258 iced_core::text::EllipsizeHeightLimit::Lines(1),
259 ))
260 .into(),
261 widget::space::horizontal().into(),
262 widget::text(key)
263 .class(key_class)
264 .ellipsize(iced_core::text::Ellipsize::Middle(
265 iced_core::text::EllipsizeHeightLimit::Lines(1),
266 ))
267 .into(),
268 ];
269
270 if let Some(icon) = icon {
271 items.insert(0, widget::icon::icon(icon).size(14).into());
272 items.insert(
273 1,
274 widget::space::horizontal().width(spacing.space_xxs).into(),
275 );
276 }
277
278 let menu_button = menu_button(items).on_press(action.message());
279
280 trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
281 }
282 MenuItem::ButtonDisabled(label, icon, action) => {
283 let l: Cow<'static, str> = label.into();
284
285 let key = find_key(&action, key_binds);
286
287 let mut items = vec![
288 widget::text(l)
289 .ellipsize(iced_core::text::Ellipsize::Middle(
290 iced_core::text::EllipsizeHeightLimit::Lines(1),
291 ))
292 .into(),
293 widget::space::horizontal().into(),
294 widget::text(key)
295 .ellipsize(iced_core::text::Ellipsize::Middle(
296 iced_core::text::EllipsizeHeightLimit::Lines(1),
297 ))
298 .class(key_class)
299 .into(),
300 ];
301
302 if let Some(icon) = icon {
303 items.insert(0, widget::icon::icon(icon).size(14).into());
304 items.insert(
305 1,
306 widget::space::horizontal().width(spacing.space_xxs).into(),
307 );
308 }
309
310 let menu_button = menu_button(items);
311
312 trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
313 }
314 MenuItem::CheckBox(label, icon, value, action) => {
315 let key = find_key(&action, key_binds);
316 let mut items = vec![
317 if value {
318 widget::icon::from_name("object-select-symbolic")
319 .size(16)
320 .icon()
321 .class(theme::Svg::Custom(Rc::new(|theme| {
322 iced_widget::svg::Style {
323 color: Some(theme.cosmic().accent_text_color().into()),
324 }
325 })))
326 .width(Length::Fixed(16.0))
327 .into()
328 } else {
329 widget::space::horizontal()
330 .width(Length::Fixed(16.0))
331 .into()
332 },
333 widget::space::horizontal().width(spacing.space_xxs).into(),
334 widget::text(label)
335 .ellipsize(iced_core::text::Ellipsize::Middle(
336 iced_core::text::EllipsizeHeightLimit::Lines(1),
337 ))
338 .align_x(iced::Alignment::Start)
339 .into(),
340 widget::space::horizontal().into(),
341 widget::text(key)
342 .class(key_class)
343 .ellipsize(iced_core::text::Ellipsize::Middle(
344 iced_core::text::EllipsizeHeightLimit::Lines(1),
345 ))
346 .into(),
347 ];
348
349 if let Some(icon) = icon {
350 items.insert(
351 1,
352 widget::space::horizontal().width(spacing.space_xxs).into(),
353 );
354 items.insert(2, widget::icon::icon(icon).size(14).into());
355 }
356
357 trees.push(MenuTree::from(Element::from(
358 menu_button(items).on_press(action.message()),
359 )));
360 }
361 MenuItem::Folder(label, children) => {
362 let l: Cow<'static, str> = label.into();
363
364 trees.push(MenuTree::<Message>::with_children(
365 RcElementWrapper::new(crate::Element::from(
366 menu_button::<'static, _>(vec![
367 widget::text(l.clone())
368 .ellipsize(iced_core::text::Ellipsize::Middle(
369 iced_core::text::EllipsizeHeightLimit::Lines(1),
370 ))
371 .into(),
372 widget::space::horizontal().into(),
373 widget::icon::from_name("pan-end-symbolic")
374 .size(16)
375 .icon()
376 .into(),
377 ])
378 .class(
379 if children.is_empty() {
381 theme::Button::MenuItem
383 } else {
384 theme::Button::MenuFolder
386 },
387 ),
388 )),
389 menu_items(key_binds, children),
390 ));
391 }
392 MenuItem::Divider => {
393 if i != size - 1 {
394 trees.push(MenuTree::<Message>::from(Element::from(
395 widget::divider::horizontal::light(),
396 )));
397 }
398 }
399 }
400 trees
401 })
402 .collect()
403}