1use super::model::{Entity, Model, Selectable};
5use crate::iced_core::id::Internal;
6use crate::theme::{SegmentedButton as Style, THEME};
7use crate::widget::dnd_destination::DragId;
8use crate::widget::menu::{
9 self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children,
10 menu_roots_diff,
11};
12use crate::widget::{Icon, icon};
13use crate::{Element, Renderer};
14use derive_setters::Setters;
15use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent};
16use iced::clipboard::mime::AllowedMimeTypes;
17use iced::touch::Finger;
18use iced::{
19 Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment,
20 event, keyboard, mouse, touch,
21};
22use iced_core::mouse::ScrollDelta;
23use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping, Wrapping};
24use iced_core::widget::{self, operation, tree};
25use iced_core::{Border, Gradient, Point, Renderer as IcedRenderer, Shadow, Text};
26use iced_core::{Clipboard, Layout, Shell, Widget, layout, renderer, widget::Tree};
27use iced_runtime::{Action, task};
28use slotmap::{Key, SecondaryMap};
29use std::borrow::Cow;
30use std::collections::HashSet;
31use std::collections::hash_map::DefaultHasher;
32use std::hash::{Hash, Hasher};
33use std::marker::PhantomData;
34use std::mem;
35use std::time::{Duration, Instant};
36
37pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
39 task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0))))
40}
41
42pub enum ItemBounds {
43 Button(Entity, Rectangle),
44 Divider(Rectangle, bool),
45}
46
47pub trait SegmentedVariant {
49 fn variant_appearance(
51 theme: &crate::Theme,
52 style: &crate::theme::SegmentedButton,
53 ) -> super::Appearance;
54
55 fn variant_bounds<'b>(
57 &'b self,
58 state: &'b LocalState,
59 bounds: Rectangle,
60 ) -> Box<dyn Iterator<Item = ItemBounds> + 'b>;
61
62 fn variant_layout(
64 &self,
65 state: &mut LocalState,
66 renderer: &crate::Renderer,
67 limits: &layout::Limits,
68 ) -> Size;
69}
70
71#[derive(Setters)]
73#[must_use]
74pub struct SegmentedButton<'a, Variant, SelectionMode, Message>
75where
76 Model<SelectionMode>: Selectable,
77 SelectionMode: Default,
78{
79 #[setters(skip)]
81 pub(super) model: &'a Model<SelectionMode>,
82 pub(super) id: Id,
84 pub(super) close_icon: Icon,
86 pub(super) scrollable_focus: bool,
88 pub(super) show_close_icon_on_hover: bool,
90 #[setters(into)]
92 pub(super) padding: Padding,
93 pub(super) dividers: bool,
95 pub(super) button_alignment: Alignment,
97 pub(super) button_padding: [u16; 4],
99 pub(super) button_height: u16,
101 pub(super) button_spacing: u16,
103 pub(super) maximum_button_width: u16,
105 pub(super) minimum_button_width: u16,
107 pub(super) indent_spacing: u16,
109 pub(super) font_active: Option<crate::font::Font>,
111 pub(super) font_hovered: Option<crate::font::Font>,
113 pub(super) font_inactive: Option<crate::font::Font>,
115 pub(super) font_size: f32,
117 pub(super) width: Length,
119 pub(super) height: Length,
121 pub(super) spacing: u16,
123 pub(super) line_height: LineHeight,
125 #[setters(into)]
127 pub(super) style: Style,
128 #[setters(skip)]
130 pub(super) context_menu: Option<Vec<menu::Tree<'a, Message, crate::Renderer>>>,
131 #[setters(skip)]
133 pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
134 #[setters(skip)]
135 pub(super) on_close: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
136 #[setters(skip)]
137 pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
138 #[setters(skip)]
139 pub(super) on_middle_press: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
140 #[setters(skip)]
141 pub(super) on_dnd_drop:
142 Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
143 pub(super) mimes: Vec<String>,
144 #[setters(skip)]
145 pub(super) on_dnd_enter: Option<Box<dyn Fn(Entity, Vec<String>) -> Message + 'static>>,
146 #[setters(skip)]
147 pub(super) on_dnd_leave: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
148 #[setters(strip_option)]
149 pub(super) drag_id: Option<DragId>,
150 #[setters(skip)]
151 variant: PhantomData<Variant>,
153}
154
155impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message>
156where
157 Self: SegmentedVariant,
158 Model<SelectionMode>: Selectable,
159 SelectionMode: Default,
160{
161 #[inline]
162 pub fn new(model: &'a Model<SelectionMode>) -> Self {
163 Self {
164 model,
165 id: Id::unique(),
166 close_icon: icon::from_name("window-close-symbolic").size(16).icon(),
167 scrollable_focus: false,
168 show_close_icon_on_hover: false,
169 button_alignment: Alignment::Start,
170 padding: Padding::from(0.0),
171 dividers: false,
172 button_padding: [0, 0, 0, 0],
173 button_height: 32,
174 button_spacing: 0,
175 minimum_button_width: u16::MIN,
176 maximum_button_width: u16::MAX,
177 indent_spacing: 16,
178 font_active: None,
179 font_hovered: None,
180 font_inactive: None,
181 font_size: 14.0,
182 height: Length::Shrink,
183 width: Length::Fill,
184 spacing: 0,
185 line_height: LineHeight::default(),
186 style: Style::default(),
187 context_menu: None,
188 on_activate: None,
189 on_close: None,
190 on_context: None,
191 on_middle_press: None,
192 on_dnd_drop: None,
193 on_dnd_enter: None,
194 on_dnd_leave: None,
195 mimes: Vec::new(),
196 variant: PhantomData,
197 drag_id: None,
198 }
199 }
200
201 pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self
202 where
203 Message: 'static,
204 {
205 self.context_menu = context_menu.map(|menus| {
206 vec![menu::Tree::with_children(
207 crate::widget::row::<'static, Message>(),
208 menus,
209 )]
210 });
211
212 if let Some(ref mut context_menu) = self.context_menu {
213 context_menu.iter_mut().for_each(menu::Tree::set_index);
214 }
215
216 self
217 }
218
219 pub fn on_activate<T>(mut self, on_activate: T) -> Self
221 where
222 T: Fn(Entity) -> Message + 'static,
223 {
224 self.on_activate = Some(Box::new(on_activate));
225 self
226 }
227
228 pub fn on_close<T>(mut self, on_close: T) -> Self
230 where
231 T: Fn(Entity) -> Message + 'static,
232 {
233 self.on_close = Some(Box::new(on_close));
234 self
235 }
236
237 pub fn on_context<T>(mut self, on_context: T) -> Self
239 where
240 T: Fn(Entity) -> Message + 'static,
241 {
242 self.on_context = Some(Box::new(on_context));
243 self
244 }
245
246 pub fn on_middle_press<T>(mut self, on_middle_press: T) -> Self
248 where
249 T: Fn(Entity) -> Message + 'static,
250 {
251 self.on_middle_press = Some(Box::new(on_middle_press));
252 self
253 }
254
255 fn is_enabled(&self, key: Entity) -> bool {
257 self.model.items.get(key).map_or(false, |item| item.enabled)
258 }
259
260 pub fn on_dnd_drop<D: AllowedMimeTypes>(
262 mut self,
263 dnd_drop_handler: impl Fn(Entity, Option<D>, DndAction) -> Message + 'static,
264 ) -> Self {
265 self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| {
266 dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action)
267 }));
268 self.mimes = D::allowed().iter().cloned().collect();
269 self
270 }
271
272 pub fn on_dnd_enter(
274 mut self,
275 dnd_enter_handler: impl Fn(Entity, Vec<String>) -> Message + 'static,
276 ) -> Self {
277 self.on_dnd_enter = Some(Box::new(dnd_enter_handler));
278 self
279 }
280
281 pub fn on_dnd_leave(mut self, dnd_leave_handler: impl Fn(Entity) -> Message + 'static) -> Self {
283 self.on_dnd_leave = Some(Box::new(dnd_leave_handler));
284 self
285 }
286
287 fn focus_previous(&mut self, state: &mut LocalState) -> event::Status {
289 match state.focused_item {
290 Item::Tab(entity) => {
291 let mut keys = self.iterate_visible_tabs(state).rev();
292
293 while let Some(key) = keys.next() {
294 if key == entity {
295 for key in keys {
296 if !self.is_enabled(key) {
298 continue;
299 }
300
301 state.focused_item = Item::Tab(key);
302 return event::Status::Captured;
303 }
304
305 break;
306 }
307 }
308
309 if self.prev_tab_sensitive(state) {
310 state.focused_item = Item::PrevButton;
311 return event::Status::Captured;
312 }
313 }
314
315 Item::NextButton => {
316 if let Some(last) = self.last_tab(state) {
317 state.focused_item = Item::Tab(last);
318 return event::Status::Captured;
319 }
320 }
321
322 Item::None => {
323 if self.next_tab_sensitive(state) {
324 state.focused_item = Item::NextButton;
325 return event::Status::Captured;
326 } else if let Some(last) = self.last_tab(state) {
327 state.focused_item = Item::Tab(last);
328 return event::Status::Captured;
329 }
330 }
331
332 Item::PrevButton | Item::Set => (),
333 }
334
335 state.focused_item = Item::None;
336 event::Status::Ignored
337 }
338
339 fn focus_next(&mut self, state: &mut LocalState) -> event::Status {
341 match state.focused_item {
342 Item::Tab(entity) => {
343 let mut keys = self.iterate_visible_tabs(state);
344 while let Some(key) = keys.next() {
345 if key == entity {
346 for key in keys {
347 if !self.is_enabled(key) {
349 continue;
350 }
351
352 state.focused_item = Item::Tab(key);
353 return event::Status::Captured;
354 }
355
356 break;
357 }
358 }
359
360 if self.next_tab_sensitive(state) {
361 state.focused_item = Item::NextButton;
362 return event::Status::Captured;
363 }
364 }
365
366 Item::PrevButton => {
367 if let Some(first) = self.first_tab(state) {
368 state.focused_item = Item::Tab(first);
369 return event::Status::Captured;
370 }
371 }
372
373 Item::None => {
374 if self.prev_tab_sensitive(state) {
375 state.focused_item = Item::PrevButton;
376 return event::Status::Captured;
377 } else if let Some(first) = self.first_tab(state) {
378 state.focused_item = Item::Tab(first);
379 return event::Status::Captured;
380 }
381 }
382
383 Item::NextButton | Item::Set => (),
384 }
385
386 state.focused_item = Item::None;
387 event::Status::Ignored
388 }
389
390 fn iterate_visible_tabs<'b>(
391 &'b self,
392 state: &LocalState,
393 ) -> impl DoubleEndedIterator<Item = Entity> + 'b {
394 self.model
395 .order
396 .iter()
397 .copied()
398 .skip(state.buttons_offset)
399 .take(state.buttons_visible)
400 }
401
402 fn first_tab(&self, state: &LocalState) -> Option<Entity> {
403 self.model.order.get(state.buttons_offset).copied()
404 }
405
406 fn last_tab(&self, state: &LocalState) -> Option<Entity> {
407 self.model
408 .order
409 .get(state.buttons_offset + state.buttons_visible)
410 .copied()
411 }
412
413 #[allow(clippy::unused_self)]
414 fn prev_tab_sensitive(&self, state: &LocalState) -> bool {
415 state.buttons_offset > 0
416 }
417
418 fn next_tab_sensitive(&self, state: &LocalState) -> bool {
419 state.buttons_offset < self.model.order.len() - state.buttons_visible
420 }
421
422 pub(super) fn button_dimensions(
423 &self,
424 state: &mut LocalState,
425 font: crate::font::Font,
426 button: Entity,
427 ) -> (f32, f32) {
428 let mut width = 0.0f32;
429 let mut icon_spacing = 0.0f32;
430
431 if let Some((text, entry)) = self
433 .model
434 .text
435 .get(button)
436 .zip(state.paragraphs.entry(button))
437 {
438 if !text.is_empty() {
439 icon_spacing = f32::from(self.button_spacing);
440 let paragraph = entry.or_insert_with(|| {
441 crate::Plain::new(Text {
442 content: text.as_ref(),
443 size: iced::Pixels(self.font_size),
444 bounds: Size::INFINITY,
445 font,
446 horizontal_alignment: alignment::Horizontal::Left,
447 vertical_alignment: alignment::Vertical::Center,
448 shaping: Shaping::Advanced,
449 wrapping: Wrapping::default(),
450 line_height: self.line_height,
451 })
452 });
453
454 let size = paragraph.min_bounds();
455 width += size.width;
456 }
457 }
458
459 if let Some(indent) = self.model.indent(button) {
461 width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width);
462 }
463
464 if let Some(icon) = self.model.icon(button) {
466 width += f32::from(icon.size) + icon_spacing;
467 } else if self.model.is_active(button) {
468 if let crate::theme::SegmentedButton::Control = self.style {
470 width += 16.0 + icon_spacing;
471 }
472 }
473
474 if self.model.is_closable(button) {
476 width += f32::from(self.close_icon.size) + f32::from(self.button_spacing);
477 }
478
479 width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]);
481 width = width.min(f32::from(self.maximum_button_width));
482
483 (width, f32::from(self.button_height))
484 }
485
486 pub(super) fn max_button_dimensions(
487 &self,
488 state: &mut LocalState,
489 renderer: &Renderer,
490 ) -> (f32, f32) {
491 let mut width = 0.0f32;
492 let mut height = 0.0f32;
493 let font = renderer.default_font();
494
495 for key in self.model.order.iter().copied() {
496 let (button_width, button_height) = self.button_dimensions(state, font, key);
497
498 state.internal_layout.push((
499 Size::new(button_width, button_height),
500 Size::new(
501 button_width
502 - f32::from(self.button_padding[0])
503 - f32::from(self.button_padding[2]),
504 button_height,
505 ),
506 ));
507
508 height = height.max(button_height);
509 width = width.max(button_width);
510 }
511
512 for (size, actual) in &mut state.internal_layout {
513 size.height = height;
514 actual.height = height;
515 }
516
517 (width, height)
518 }
519
520 fn button_is_focused(&self, state: &LocalState, key: Entity) -> bool {
521 self.on_activate.is_some() && Item::Tab(key) == state.focused_item
522 }
523
524 fn button_is_hovered(&self, state: &LocalState, key: Entity) -> bool {
525 self.on_activate.is_some() && state.hovered == Item::Tab(key)
526 || state
527 .dnd_state
528 .drag_offer
529 .as_ref()
530 .is_some_and(|id| id.data.is_some_and(|d| d == key))
531 }
532
533 #[must_use]
538 pub fn get_drag_id(&self) -> u128 {
539 self.drag_id.map_or_else(
540 || {
541 u128::from(match &self.id.0.0 {
542 Internal::Unique(id) | Internal::Custom(id, _) => *id,
543 Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."),
544 })
545 },
546 |id| id.0,
547 )
548 }
549}
550
551impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
552 for SegmentedButton<'_, Variant, SelectionMode, Message>
553where
554 Self: SegmentedVariant,
555 Model<SelectionMode>: Selectable,
556 SelectionMode: Default,
557 Message: 'static + Clone,
558{
559 fn children(&self) -> Vec<Tree> {
560 let mut children = Vec::new();
561
562 if let Some(ref context_menu) = self.context_menu {
564 let mut tree = Tree::empty();
565 tree.state = tree::State::new(MenuBarState::default());
566 tree.children = menu_roots_children(context_menu);
567 children.push(tree);
568 }
569
570 children
571 }
572
573 fn tag(&self) -> tree::Tag {
574 tree::Tag::of::<LocalState>()
575 }
576
577 fn state(&self) -> tree::State {
578 #[allow(clippy::default_trait_access)]
579 tree::State::new(LocalState {
580 paragraphs: SecondaryMap::new(),
581 text_hashes: SecondaryMap::new(),
582 buttons_visible: Default::default(),
583 buttons_offset: Default::default(),
584 collapsed: Default::default(),
585 focused: Default::default(),
586 focused_item: Default::default(),
587 hovered: Default::default(),
588 known_length: Default::default(),
589 middle_clicked: Default::default(),
590 internal_layout: Default::default(),
591 context_cursor: Point::default(),
592 show_context: Default::default(),
593 wheel_timestamp: Default::default(),
594 dnd_state: Default::default(),
595 fingers_pressed: Default::default(),
596 pressed_item: None,
597 })
598 }
599
600 fn diff(&mut self, tree: &mut Tree) {
601 let state = tree.state.downcast_mut::<LocalState>();
602
603 for key in self.model.order.iter().copied() {
604 if let Some(text) = self.model.text.get(key) {
605 let (font, button_state) =
606 if self.model.is_active(key) || self.button_is_focused(state, key) {
607 (self.font_active, 0)
608 } else if self.button_is_hovered(state, key) {
609 (self.font_hovered, 1)
610 } else {
611 (self.font_inactive, 2)
612 };
613
614 let font = font.unwrap_or_else(crate::font::default);
615 let mut hasher = DefaultHasher::new();
616 text.hash(&mut hasher);
617 font.hash(&mut hasher);
618 button_state.hash(&mut hasher);
619 let text_hash = hasher.finish();
620
621 if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) {
622 if prev_hash == text_hash {
623 continue;
624 }
625 }
626
627 let text = Text {
628 content: text.as_ref(),
629 size: iced::Pixels(self.font_size),
630 bounds: Size::INFINITY,
631 font,
632 horizontal_alignment: alignment::Horizontal::Left,
633 vertical_alignment: alignment::Vertical::Center,
634 shaping: Shaping::Advanced,
635 wrapping: Wrapping::default(),
636 line_height: self.line_height,
637 };
638
639 if let Some(paragraph) = state.paragraphs.get_mut(key) {
640 paragraph.update(text);
641 } else {
642 state.paragraphs.insert(key, crate::Plain::new(text));
643 }
644 }
645 }
646
647 if let Some(context_menu) = &mut self.context_menu {
649 if tree.children.is_empty() {
650 let mut child_tree = Tree::empty();
651 child_tree.state = tree::State::new(MenuBarState::default());
652 tree.children.push(child_tree);
653 } else {
654 tree.children.truncate(1);
655 }
656 menu_roots_diff(context_menu, &mut tree.children[0]);
657 } else {
658 tree.children.clear();
659 }
660 }
661
662 fn size(&self) -> Size<Length> {
663 Size::new(self.width, self.height)
664 }
665
666 fn layout(
667 &self,
668 tree: &mut Tree,
669 renderer: &Renderer,
670 limits: &layout::Limits,
671 ) -> layout::Node {
672 let state = tree.state.downcast_mut::<LocalState>();
673 let limits = limits.shrink(self.padding);
674 let size = self
675 .variant_layout(state, renderer, &limits)
676 .expand(self.padding);
677 layout::Node::new(size)
678 }
679
680 #[allow(clippy::too_many_lines)]
681 fn on_event(
682 &mut self,
683 tree: &mut Tree,
684 mut event: Event,
685 layout: Layout<'_>,
686 cursor_position: mouse::Cursor,
687 _renderer: &Renderer,
688 _clipboard: &mut dyn Clipboard,
689 shell: &mut Shell<'_, Message>,
690 _viewport: &iced::Rectangle,
691 ) -> event::Status {
692 let bounds = layout.bounds();
693 let state = tree.state.downcast_mut::<LocalState>();
694 state.hovered = Item::None;
695
696 let my_id = self.get_drag_id();
697
698 if let Event::Dnd(e) = &mut event {
699 let entity = state
700 .dnd_state
701 .drag_offer
702 .as_ref()
703 .map(|dnd_state| dnd_state.data);
704 match e {
705 DndEvent::Offer(
706 id,
707 OfferEvent::Enter {
708 x, y, mime_types, ..
709 },
710 ) if Some(my_id) == *id => {
711 let entity = self
712 .variant_bounds(state, bounds)
713 .filter_map(|item| match item {
714 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
715 _ => None,
716 })
717 .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
718 .map(|(key, _)| key);
719
720 let on_dnd_enter =
721 self.on_dnd_enter
722 .as_ref()
723 .zip(entity)
724 .map(|(on_enter, entity)| {
725 move |_, _, mime_types| on_enter(entity, mime_types)
726 });
727
728 _ = state.dnd_state.on_enter::<Message>(
729 *x,
730 *y,
731 mime_types.clone(),
732 on_dnd_enter,
733 entity,
734 );
735 }
736 DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination)
737 if Some(my_id) == *id =>
738 {
739 if let Some(Some(entity)) = entity {
740 if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
741 shell.publish(on_dnd_leave(entity));
742 }
743 }
744 _ = state.dnd_state.on_leave::<Message>(None);
745 }
746 DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => {
747 let new = self
748 .variant_bounds(state, bounds)
749 .filter_map(|item| match item {
750 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
751 _ => None,
752 })
753 .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
754 .map(|(key, _)| key);
755 if let Some(new_entity) = new {
756 state.dnd_state.on_motion::<Message>(
757 *x,
758 *y,
759 None::<fn(_, _) -> Message>,
760 None::<fn(_, _, _) -> Message>,
761 Some(new_entity),
762 );
763 if Some(Some(new_entity)) != entity {
764 let prev_action = state
765 .dnd_state
766 .drag_offer
767 .as_ref()
768 .map(|dnd| dnd.selected_action);
769 if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() {
770 shell.publish(on_dnd_enter(new_entity, Vec::new()));
771 }
772 if let Some(dnd) = state.dnd_state.drag_offer.as_mut() {
773 dnd.data = Some(new_entity);
774 if let Some(prev_action) = prev_action {
775 dnd.selected_action = prev_action;
776 }
777 }
778 }
779 } else if entity.is_some() {
780 state.dnd_state.on_motion::<Message>(
781 *x,
782 *y,
783 None::<fn(_, _) -> Message>,
784 None::<fn(_, _, _) -> Message>,
785 None,
786 );
787 if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
788 if let Some(Some(entity)) = entity {
789 shell.publish(on_dnd_leave(entity));
790 }
791 }
792 }
793 }
794 DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => {
795 _ = state
796 .dnd_state
797 .on_drop::<Message>(None::<fn(_, _) -> Message>);
798 }
799 DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
800 if state.dnd_state.drag_offer.is_some() {
801 _ = state
802 .dnd_state
803 .on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
804 }
805 }
806 DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
807 if let Some(Some(entity)) = entity {
808 let on_drop = self.on_dnd_drop.as_ref();
809 let on_drop = on_drop.map(|on_drop| {
810 |mime, data, action, _, _| on_drop(entity, data, mime, action)
811 });
812
813 if let (Some(msg), ret) = state.dnd_state.on_data_received(
814 mem::take(mime_type),
815 mem::take(data),
816 None::<fn(_, _) -> Message>,
817 on_drop,
818 ) {
819 shell.publish(msg);
820 return ret;
821 }
822 }
823 }
824 _ => {}
825 }
826 }
827
828 if cursor_position.is_over(bounds) {
829 let fingers_pressed = state.fingers_pressed.len();
830
831 match event {
832 Event::Touch(touch::Event::FingerPressed { id, .. }) => {
833 state.fingers_pressed.insert(id);
834 }
835
836 Event::Touch(touch::Event::FingerLifted { id, .. }) => {
837 state.fingers_pressed.remove(&id);
838 }
839
840 _ => (),
841 }
842
843 if state.collapsed {
845 if cursor_position.is_over(prev_tab_bounds(&bounds, f32::from(self.button_height)))
847 && self.prev_tab_sensitive(state)
848 {
849 state.hovered = Item::PrevButton;
850 if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
851 | Event::Touch(touch::Event::FingerLifted { .. }) = event
852 {
853 state.buttons_offset -= 1;
854 }
855 } else {
856 if cursor_position
858 .is_over(next_tab_bounds(&bounds, f32::from(self.button_height)))
859 && self.next_tab_sensitive(state)
860 {
861 state.hovered = Item::NextButton;
862
863 if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
864 | Event::Touch(touch::Event::FingerLifted { .. }) = event
865 {
866 state.buttons_offset += 1;
867 }
868 }
869 }
870 }
871
872 for (key, bounds) in self
873 .variant_bounds(state, bounds)
874 .filter_map(|item| match item {
875 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
876 _ => None,
877 })
878 .collect::<Vec<_>>()
879 {
880 if cursor_position.is_over(bounds) {
881 if self.model.items[key].enabled {
882 state.hovered = Item::Tab(key);
884
885 if self.model.items[key].closable {
887 if let Some(on_close) = self.on_close.as_ref() {
889 if cursor_position
890 .is_over(close_bounds(bounds, f32::from(self.close_icon.size)))
891 && (left_button_released(&event)
892 || (touch_lifted(&event) && fingers_pressed == 1))
893 {
894 shell.publish(on_close(key));
895 return event::Status::Captured;
896 }
897
898 if self.on_middle_press.is_none() {
899 if let Event::Mouse(mouse::Event::ButtonReleased(
901 mouse::Button::Middle,
902 )) = event
903 {
904 if state.middle_clicked == Some(Item::Tab(key)) {
905 shell.publish(on_close(key));
906 return event::Status::Captured;
907 }
908
909 state.middle_clicked = None;
910 }
911 }
912 }
913 }
914
915 if let Event::Mouse(mouse::Event::ButtonReleased(_))
916 | Event::Touch(touch::Event::FingerLifted { .. }) = event
917 {
918 state.focused = false;
919 state.focused_item = Item::None;
920 }
921
922 if let Some(on_activate) = self.on_activate.as_ref() {
923 if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
924 | Event::Touch(touch::Event::FingerPressed { .. }) = event
925 {
926 state.pressed_item = Some(Item::Tab(key));
927 } else if let Event::Mouse(mouse::Event::ButtonReleased(
928 mouse::Button::Left,
929 ))
930 | Event::Touch(touch::Event::FingerLifted { .. }) = event
931 {
932 let mut can_activate = true;
933 if state.pressed_item != Some(Item::Tab(key)) {
934 can_activate = false;
935 }
936
937 if can_activate {
938 shell.publish(on_activate(key));
939 state.focused = true;
940 state.focused_item = Item::Tab(key);
941 state.pressed_item = None;
942 return event::Status::Captured;
943 }
944 }
945 }
946
947 if self.context_menu.is_some() {
949 if let Some(on_context) = self.on_context.as_ref() {
950 if right_button_released(&event)
951 || (touch_lifted(&event) && fingers_pressed == 2)
952 {
953 state.show_context = Some(key);
954 state.context_cursor =
955 cursor_position.position().unwrap_or_default();
956
957 let menu_state =
958 tree.children[0].state.downcast_mut::<MenuBarState>();
959 menu_state.open = true;
960 menu_state.view_cursor = cursor_position;
961
962 shell.publish(on_context(key));
963 return event::Status::Captured;
964 }
965 }
966 }
967
968 if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) =
969 event
970 {
971 state.middle_clicked = Some(Item::Tab(key));
972 if let Some(on_middle_press) = self.on_middle_press.as_ref() {
973 shell.publish(on_middle_press(key));
974 return event::Status::Captured;
975 }
976 }
977 }
978
979 break;
980 }
981 }
982
983 if self.scrollable_focus {
984 if let Some(on_activate) = self.on_activate.as_ref() {
985 if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
986 let current = Instant::now();
987
988 if state.wheel_timestamp.map_or(true, |previous| {
990 current.duration_since(previous) > Duration::from_millis(250)
991 }) {
992 state.wheel_timestamp = Some(current);
993
994 match delta {
995 ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => {
996 let mut activate_key = None;
997
998 if y < 0.0 {
999 let mut prev_key = Entity::null();
1000
1001 for key in self.model.order.iter().copied() {
1002 if self.model.is_active(key) && !prev_key.is_null() {
1003 activate_key = Some(prev_key);
1004 }
1005
1006 if self.model.is_enabled(key) {
1007 prev_key = key;
1008 }
1009 }
1010 } else if y > 0.0 {
1011 let mut buttons = self.model.order.iter().copied();
1012 while let Some(key) = buttons.next() {
1013 if self.model.is_active(key) {
1014 for key in buttons {
1015 if self.model.is_enabled(key) {
1016 activate_key = Some(key);
1017 break;
1018 }
1019 }
1020 break;
1021 }
1022 }
1023 }
1024
1025 if let Some(key) = activate_key {
1026 shell.publish(on_activate(key));
1027 state.focused = true;
1028 state.focused_item = Item::Tab(key);
1029 return event::Status::Captured;
1030 }
1031 }
1032 }
1033 }
1034 }
1035 }
1036 }
1037 }
1038
1039 if state.focused {
1040 if let Event::Keyboard(keyboard::Event::KeyPressed {
1041 key: keyboard::Key::Named(keyboard::key::Named::Tab),
1042 modifiers,
1043 ..
1044 }) = event
1045 {
1046 return if modifiers.shift() {
1047 self.focus_previous(state)
1048 } else {
1049 self.focus_next(state)
1050 };
1051 }
1052
1053 if let Some(on_activate) = self.on_activate.as_ref() {
1054 if let Event::Keyboard(keyboard::Event::KeyReleased {
1055 key: keyboard::Key::Named(keyboard::key::Named::Enter),
1056 ..
1057 }) = event
1058 {
1059 match state.focused_item {
1060 Item::Tab(entity) => {
1061 shell.publish(on_activate(entity));
1062 }
1063
1064 Item::PrevButton => {
1065 if self.prev_tab_sensitive(state) {
1066 state.buttons_offset -= 1;
1067
1068 if !self.prev_tab_sensitive(state) {
1070 if let Some(first) = self.first_tab(state) {
1071 state.focused_item = Item::Tab(first);
1072 }
1073 }
1074 }
1075 }
1076
1077 Item::NextButton => {
1078 if self.next_tab_sensitive(state) {
1079 state.buttons_offset += 1;
1080
1081 if !self.next_tab_sensitive(state) {
1083 if let Some(last) = self.last_tab(state) {
1084 state.focused_item = Item::Tab(last);
1085 }
1086 }
1087 }
1088 }
1089
1090 Item::None | Item::Set => (),
1091 }
1092
1093 return event::Status::Captured;
1094 }
1095 }
1096 }
1097
1098 event::Status::Ignored
1099 }
1100
1101 fn operate(
1102 &self,
1103 tree: &mut Tree,
1104 _layout: Layout<'_>,
1105 _renderer: &Renderer,
1106 operation: &mut dyn iced_core::widget::Operation<()>,
1107 ) {
1108 let state = tree.state.downcast_mut::<LocalState>();
1109 operation.focusable(state, Some(&self.id.0));
1110
1111 if let Item::Set = state.focused_item {
1112 if self.prev_tab_sensitive(state) {
1113 state.focused_item = Item::PrevButton;
1114 } else if let Some(first) = self.first_tab(state) {
1115 state.focused_item = Item::Tab(first);
1116 }
1117 }
1118 }
1119
1120 fn mouse_interaction(
1121 &self,
1122 tree: &Tree,
1123 layout: Layout<'_>,
1124 cursor_position: mouse::Cursor,
1125 _viewport: &iced::Rectangle,
1126 _renderer: &Renderer,
1127 ) -> iced_core::mouse::Interaction {
1128 if self.on_activate.is_none() {
1129 return iced_core::mouse::Interaction::default();
1130 }
1131 let state = tree.state.downcast_ref::<LocalState>();
1132 let bounds = layout.bounds();
1133
1134 if cursor_position.is_over(bounds) {
1135 let hovered_button = self
1136 .variant_bounds(state, bounds)
1137 .filter_map(|item| match item {
1138 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1139 _ => None,
1140 })
1141 .find(|(_key, bounds)| cursor_position.is_over(*bounds));
1142
1143 if let Some((key, _bounds)) = hovered_button {
1144 return if self.model.items[key].enabled {
1145 iced_core::mouse::Interaction::Pointer
1146 } else {
1147 iced_core::mouse::Interaction::Idle
1148 };
1149 }
1150 }
1151
1152 iced_core::mouse::Interaction::Idle
1153 }
1154
1155 #[allow(clippy::too_many_lines)]
1156 fn draw(
1157 &self,
1158 tree: &Tree,
1159 renderer: &mut Renderer,
1160 theme: &crate::Theme,
1161 style: &renderer::Style,
1162 layout: Layout<'_>,
1163 cursor: mouse::Cursor,
1164 viewport: &iced::Rectangle,
1165 ) {
1166 let state = tree.state.downcast_ref::<LocalState>();
1167 let appearance = Self::variant_appearance(theme, &self.style);
1168 let bounds: Rectangle = layout.bounds();
1169 let button_amount = self.model.items.len();
1170
1171 let apply_alpha = |mut c: Color| {
1173 if self.on_activate.is_none() {
1174 c.a /= 2.0;
1175 }
1176
1177 c
1178 };
1179
1180 let bg_with_alpha = |mut b| {
1182 match &mut b {
1183 Background::Color(c) => {
1184 *c = apply_alpha(*c);
1185 }
1186
1187 Background::Gradient(g) => {
1188 let Gradient::Linear(l) = g;
1189 for c in &mut l.stops {
1190 let Some(stop) = c else {
1191 continue;
1192 };
1193 stop.color = apply_alpha(stop.color);
1194 }
1195 }
1196 }
1197 b
1198 };
1199
1200 if let Some(background) = appearance.background {
1202 renderer.fill_quad(
1203 renderer::Quad {
1204 bounds,
1205 border: Border {
1206 radius: appearance.border_radius,
1207 ..Border::default()
1208 },
1209 shadow: Shadow::default(),
1210 },
1211 bg_with_alpha(background),
1212 );
1213 }
1214
1215 if state.collapsed {
1217 let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height));
1218
1219 let mut background_appearance =
1221 if self.on_activate.is_some() && Item::PrevButton == state.focused_item {
1222 Some(appearance.focus)
1223 } else if self.on_activate.is_some() && Item::PrevButton == state.hovered {
1224 Some(appearance.hover)
1225 } else {
1226 None
1227 };
1228
1229 if let Some(background_appearance) = background_appearance.take() {
1230 renderer.fill_quad(
1231 renderer::Quad {
1232 bounds: tab_bounds,
1233 border: Border {
1234 radius: theme.cosmic().radius_s().into(),
1235 ..Default::default()
1236 },
1237 shadow: Shadow::default(),
1238 },
1239 background_appearance
1240 .background
1241 .map_or(Background::Color(Color::TRANSPARENT), bg_with_alpha),
1242 );
1243 }
1244
1245 draw_icon::<Message>(
1246 renderer,
1247 theme,
1248 style,
1249 cursor,
1250 viewport,
1251 apply_alpha(if state.buttons_offset == 0 {
1252 appearance.inactive.text_color
1253 } else if let Item::PrevButton = state.focused_item {
1254 appearance.focus.text_color
1255 } else {
1256 appearance.active.text_color
1257 }),
1258 Rectangle {
1259 x: tab_bounds.x + 8.0,
1260 y: tab_bounds.y + f32::from(self.button_height) / 4.0,
1261 width: 16.0,
1262 height: 16.0,
1263 },
1264 icon::from_name("go-previous-symbolic").size(16).icon(),
1265 );
1266
1267 tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height));
1268
1269 background_appearance =
1271 if self.on_activate.is_some() && Item::NextButton == state.focused_item {
1272 Some(appearance.focus)
1273 } else if self.on_activate.is_some() && Item::NextButton == state.hovered {
1274 Some(appearance.hover)
1275 } else {
1276 None
1277 };
1278
1279 if let Some(background_appearance) = background_appearance {
1280 renderer.fill_quad(
1281 renderer::Quad {
1282 bounds: tab_bounds,
1283 border: Border {
1284 radius: theme.cosmic().radius_s().into(),
1285 ..Default::default()
1286 },
1287 shadow: Shadow::default(),
1288 },
1289 background_appearance
1290 .background
1291 .unwrap_or(Background::Color(Color::TRANSPARENT)),
1292 );
1293 }
1294
1295 draw_icon::<Message>(
1296 renderer,
1297 theme,
1298 style,
1299 cursor,
1300 viewport,
1301 apply_alpha(if self.next_tab_sensitive(state) {
1302 appearance.active.text_color
1303 } else if let Item::NextButton = state.focused_item {
1304 appearance.focus.text_color
1305 } else {
1306 appearance.inactive.text_color
1307 }),
1308 Rectangle {
1309 x: tab_bounds.x + 8.0,
1310 y: tab_bounds.y + f32::from(self.button_height) / 4.0,
1311 width: 16.0,
1312 height: 16.0,
1313 },
1314 icon::from_name("go-next-symbolic").size(16).icon(),
1315 );
1316 }
1317
1318 let mut nth = 0;
1320 self.variant_bounds(state, bounds).for_each(move |item| {
1321 let (key, mut bounds) = match item {
1322 ItemBounds::Button(entity, bounds) => (entity, bounds),
1324
1325 ItemBounds::Divider(bounds, accented) => {
1327 renderer.fill_quad(
1328 renderer::Quad {
1329 bounds,
1330 border: Border::default(),
1331 shadow: Shadow::default(),
1332 },
1333 {
1334 let theme = crate::theme::active();
1335 if accented {
1336 Background::Color(theme.cosmic().small_widget_divider().into())
1337 } else {
1338 Background::Color(theme.cosmic().primary_container_divider().into())
1339 }
1340 },
1341 );
1342
1343 return;
1344 }
1345 };
1346
1347 let center_y = bounds.center_y();
1348
1349 let menu_open = !tree.children.is_empty()
1350 && tree.children[0].state.downcast_ref::<MenuBarState>().open;
1351
1352 let key_is_active = self.model.is_active(key);
1353 let key_is_hovered = self.button_is_hovered(state, key);
1354 let key_has_context_menu_open = menu_open && state.show_context == Some(key);
1355 let status_appearance = if self.button_is_focused(state, key) {
1356 appearance.focus
1357 } else if key_is_active {
1358 appearance.active
1359 } else if key_is_hovered || key_has_context_menu_open {
1360 appearance.hover
1361 } else {
1362 appearance.inactive
1363 };
1364
1365 let button_appearance = if nth == 0 {
1366 status_appearance.first
1367 } else if nth + 1 == button_amount {
1368 status_appearance.last
1369 } else {
1370 status_appearance.middle
1371 };
1372
1373 if status_appearance.background.is_some() {
1375 renderer.fill_quad(
1376 renderer::Quad {
1377 bounds,
1378 border: Border {
1379 radius: button_appearance.border_radius,
1380 ..Default::default()
1381 },
1382 shadow: Shadow::default(),
1383 },
1384 status_appearance
1385 .background
1386 .map_or(Background::Color(Color::TRANSPARENT), bg_with_alpha),
1387 );
1388 }
1389
1390 if let Some((width, background)) = button_appearance.border_bottom {
1392 let mut bounds = bounds;
1393 bounds.y = bounds.y + bounds.height - width;
1394 bounds.height = width;
1395
1396 let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
1397 renderer.fill_quad(
1398 renderer::Quad {
1399 bounds,
1400 border: Border {
1401 radius: rad_0.into(),
1402 ..Default::default()
1403 },
1404 shadow: Shadow::default(),
1405 },
1406 bg_with_alpha(background.into()),
1407 );
1408 }
1409
1410 let original_bounds = bounds;
1411
1412 bounds.x += f32::from(self.button_padding[0]);
1413 bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
1414
1415 if let Some(indent) = self.model.indent(key) {
1417 let adjustment = f32::from(indent) * f32::from(self.indent_spacing);
1418 bounds.x += adjustment;
1419 bounds.width -= adjustment;
1420 }
1421
1422 {
1424 let actual_width = state.internal_layout[nth].1.width;
1425
1426 let offset = match self.button_alignment {
1427 Alignment::Start => None,
1428 Alignment::Center => Some((bounds.width - actual_width) / 2.0),
1429 Alignment::End => Some(bounds.width - actual_width),
1430 };
1431
1432 if let Some(offset) = offset {
1433 bounds.x += offset - f32::from(self.button_padding[0]);
1434 bounds.width = actual_width;
1435 }
1436 }
1437
1438 if let Some(icon) = self.model.icon(key) {
1440 let mut image_bounds = bounds;
1441 let width = f32::from(icon.size);
1442 let offset = width + f32::from(self.button_spacing);
1443 image_bounds.y = center_y - width / 2.0;
1444
1445 draw_icon::<Message>(
1446 renderer,
1447 theme,
1448 style,
1449 cursor,
1450 viewport,
1451 apply_alpha(status_appearance.text_color),
1452 Rectangle {
1453 width,
1454 height: width,
1455 ..image_bounds
1456 },
1457 icon.clone(),
1458 );
1459
1460 bounds.x += offset;
1461 } else {
1462 if key_is_active {
1464 if let crate::theme::SegmentedButton::Control = self.style {
1465 let mut image_bounds = bounds;
1466 image_bounds.y = center_y - 16.0 / 2.0;
1467
1468 draw_icon::<Message>(
1469 renderer,
1470 theme,
1471 style,
1472 cursor,
1473 viewport,
1474 apply_alpha(status_appearance.text_color),
1475 Rectangle {
1476 width: 16.0,
1477 height: 16.0,
1478 ..image_bounds
1479 },
1480 crate::widget::icon(
1481 match crate::widget::common::object_select().data() {
1482 crate::iced_core::svg::Data::Bytes(bytes) => {
1483 crate::widget::icon::from_svg_bytes(bytes.as_ref())
1484 }
1485 crate::iced_core::svg::Data::Path(path) => {
1486 crate::widget::icon::from_path(path.clone())
1487 }
1488 },
1489 ),
1490 );
1491
1492 let offset = 16.0 + f32::from(self.button_spacing);
1493
1494 bounds.x += offset;
1495 }
1496 }
1497 }
1498
1499 let show_close_button =
1501 (key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
1502 && self.model.is_closable(key);
1503
1504 let close_icon_width = if show_close_button {
1506 f32::from(self.close_icon.size)
1507 } else {
1508 0.0
1509 };
1510
1511 bounds.width = original_bounds.width
1512 - (bounds.x - original_bounds.x)
1513 - close_icon_width
1514 - f32::from(self.button_padding[2]);
1515
1516 bounds.y = center_y;
1517
1518 if self.model.text(key).is_some_and(|text| !text.is_empty()) {
1519 renderer.fill_paragraph(
1521 state.paragraphs[key].raw(),
1522 bounds.position(),
1523 apply_alpha(status_appearance.text_color),
1524 Rectangle {
1525 x: bounds.x,
1526 width: bounds.width,
1527 ..original_bounds
1528 },
1529 );
1530 }
1531
1532 if show_close_button {
1534 let close_button_bounds = close_bounds(original_bounds, close_icon_width);
1535
1536 draw_icon::<Message>(
1537 renderer,
1538 theme,
1539 style,
1540 cursor,
1541 viewport,
1542 apply_alpha(status_appearance.text_color),
1543 close_button_bounds,
1544 self.close_icon.clone(),
1545 );
1546 }
1547
1548 nth += 1;
1549 });
1550 }
1551
1552 fn overlay<'b>(
1553 &'b mut self,
1554 tree: &'b mut Tree,
1555 layout: iced_core::Layout<'_>,
1556 _renderer: &Renderer,
1557 translation: Vector,
1558 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
1559 let state = tree.state.downcast_ref::<LocalState>();
1560
1561 let Some(entity) = state.show_context else {
1562 return None;
1563 };
1564
1565 let bounds = self
1566 .variant_bounds(state, layout.bounds())
1567 .find_map(|item| match item {
1568 ItemBounds::Button(e, bounds) if e == entity => Some(bounds),
1569 _ => None,
1570 });
1571 let Some(mut bounds) = bounds else {
1572 return None;
1573 };
1574
1575 let Some(context_menu) = self.context_menu.as_mut() else {
1576 return None;
1577 };
1578
1579 if !tree.children[0].state.downcast_ref::<MenuBarState>().open {
1580 return None;
1581 }
1582
1583 bounds.x = state.context_cursor.x;
1584 bounds.y = state.context_cursor.y;
1585
1586 Some(
1587 crate::widget::menu::Menu {
1588 tree: &mut tree.children[0],
1589 menu_roots: context_menu,
1590 bounds_expand: 16,
1591 menu_overlays_parent: true,
1592 close_condition: CloseCondition {
1593 leave: false,
1594 click_outside: true,
1595 click_inside: true,
1596 },
1597 item_width: ItemWidth::Uniform(240),
1598 item_height: ItemHeight::Dynamic(40),
1599 bar_bounds: bounds,
1600 main_offset: -(bounds.height as i32),
1601 cross_offset: 0,
1602 root_bounds_list: vec![bounds],
1603 path_highlight: Some(PathHighlight::MenuActive),
1604 style: &crate::theme::menu_bar::MenuBarStyle::Default,
1605 position: Point::new(translation.x, translation.y),
1606 }
1607 .overlay(),
1608 )
1609 }
1610
1611 fn drag_destinations(
1612 &self,
1613 _state: &Tree,
1614 layout: Layout<'_>,
1615 _renderer: &Renderer,
1616 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
1617 ) {
1618 let bounds = layout.bounds();
1619
1620 let my_id = self.get_drag_id();
1621 let dnd_rect = DndDestinationRectangle {
1622 id: my_id,
1623 rectangle: dnd::Rectangle {
1624 x: f64::from(bounds.x),
1625 y: f64::from(bounds.y),
1626 width: f64::from(bounds.width),
1627 height: f64::from(bounds.height),
1628 },
1629 mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
1630 actions: DndAction::Copy | DndAction::Move,
1631 preferred: DndAction::Move,
1632 };
1633 dnd_rectangles.push(dnd_rect);
1634 }
1635}
1636
1637impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
1638 for Element<'a, Message>
1639where
1640 SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
1641 Variant: 'static,
1642 Model<SelectionMode>: Selectable,
1643 SelectionMode: Default,
1644 Message: 'static + Clone,
1645{
1646 fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self {
1647 if widget.model.items.is_empty() {
1648 widget.spacing = 0;
1649 }
1650
1651 Self::new(widget)
1652 }
1653}
1654
1655pub struct LocalState {
1657 pub(super) buttons_visible: usize,
1659 pub(super) buttons_offset: usize,
1661 pub(super) collapsed: bool,
1663 focused: bool,
1665 focused_item: Item,
1667 hovered: Item,
1669 middle_clicked: Option<Item>,
1671 pub(super) known_length: usize,
1673 pub(super) internal_layout: Vec<(Size, Size)>,
1675 paragraphs: SecondaryMap<Entity, crate::Plain>,
1677 text_hashes: SecondaryMap<Entity, u64>,
1679 context_cursor: Point,
1681 show_context: Option<Entity>,
1683 wheel_timestamp: Option<Instant>,
1685 pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
1687 fingers_pressed: HashSet<Finger>,
1689 pressed_item: Option<Item>,
1691}
1692
1693#[derive(Debug, Default, PartialEq)]
1694enum Item {
1695 NextButton,
1696 #[default]
1697 None,
1698 PrevButton,
1699 Set,
1700 Tab(Entity),
1701}
1702
1703impl operation::Focusable for LocalState {
1704 fn is_focused(&self) -> bool {
1705 self.focused
1706 }
1707
1708 fn focus(&mut self) {
1709 self.focused = true;
1710 self.focused_item = Item::Set;
1711 }
1712
1713 fn unfocus(&mut self) {
1714 self.focused = false;
1715 self.focused_item = Item::None;
1716 self.show_context = None;
1717 }
1718}
1719
1720#[derive(Debug, Clone, PartialEq)]
1722pub struct Id(widget::Id);
1723
1724impl Id {
1725 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
1727 Self(widget::Id::new(id))
1728 }
1729
1730 #[must_use]
1734 #[inline]
1735 pub fn unique() -> Self {
1736 Self(widget::Id::unique())
1737 }
1738}
1739
1740impl From<Id> for widget::Id {
1741 fn from(id: Id) -> Self {
1742 id.0
1743 }
1744}
1745
1746fn close_bounds(area: Rectangle<f32>, icon_size: f32) -> Rectangle<f32> {
1748 Rectangle {
1749 x: area.x + area.width - icon_size - 8.0,
1750 y: area.center_y() - (icon_size / 2.0),
1751 width: icon_size,
1752 height: icon_size,
1753 }
1754}
1755
1756fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
1758 Rectangle {
1759 x: bounds.x + bounds.width - button_height,
1760 y: bounds.y,
1761 width: button_height,
1762 height: button_height,
1763 }
1764}
1765
1766fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
1768 Rectangle {
1769 x: bounds.x,
1770 y: bounds.y,
1771 width: button_height,
1772 height: button_height,
1773 }
1774}
1775
1776#[allow(clippy::too_many_arguments)]
1777fn draw_icon<Message: 'static>(
1778 renderer: &mut Renderer,
1779 theme: &crate::Theme,
1780 style: &renderer::Style,
1781 cursor: mouse::Cursor,
1782 viewport: &Rectangle,
1783 color: Color,
1784 bounds: Rectangle,
1785 icon: Icon,
1786) {
1787 let layout_node = layout::Node::new(Size {
1788 width: bounds.width,
1789 height: bounds.width,
1790 })
1791 .move_to(Point {
1792 x: bounds.x,
1793 y: bounds.y,
1794 });
1795
1796 Widget::<Message, crate::Theme, Renderer>::draw(
1797 Element::<Message>::from(icon.clone()).as_widget(),
1798 &Tree::empty(),
1799 renderer,
1800 theme,
1801 &renderer::Style {
1802 icon_color: color,
1803 text_color: color,
1804 scale_factor: style.scale_factor,
1805 },
1806 Layout::new(&layout_node),
1807 cursor,
1808 viewport,
1809 );
1810}
1811
1812fn left_button_released(event: &Event) -> bool {
1813 matches!(
1814 event,
1815 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
1816 )
1817}
1818
1819fn right_button_released(event: &Event) -> bool {
1820 matches!(
1821 event,
1822 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,))
1823 )
1824}
1825
1826fn touch_lifted(event: &Event) -> bool {
1827 matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
1828}