1use super::model::{Entity, Model, Selectable};
5use super::{InsertPosition, ReorderEvent};
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::{
16 self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent,
17};
18use iced::clipboard::mime::AllowedMimeTypes;
19use iced::touch::Finger;
20use iced::{
21 Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment,
22 keyboard, mouse, touch, window,
23};
24use iced_core::id::Internal;
25use iced_core::mouse::ScrollDelta;
26use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping};
27use iced_core::widget::operation::Focusable;
28use iced_core::widget::{self, operation, tree};
29use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text};
30use iced_core::{Clipboard, Layout, Shell, Widget, layout, renderer, widget::Tree};
31use iced_runtime::{Action, task};
32use slotmap::{Key, SecondaryMap};
33use std::borrow::Cow;
34use std::cell::{Cell, LazyCell};
35use std::collections::HashSet;
36use std::collections::hash_map::DefaultHasher;
37use std::hash::{Hash, Hasher};
38use std::marker::PhantomData;
39use std::time::{Duration, Instant};
40
41thread_local! {
42 static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now()));
44}
45
46const TAB_REORDER_LOG_TARGET: &str = "libcosmic::widget::tab_reorder";
47
48pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
50 task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0))))
51}
52
53pub enum ItemBounds {
54 Button(Entity, Rectangle),
55 Divider(Rectangle, bool),
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59enum DropSide {
60 Before,
61 After,
62}
63
64impl From<DropSide> for InsertPosition {
65 fn from(side: DropSide) -> Self {
66 match side {
67 DropSide::Before => InsertPosition::Before,
68 DropSide::After => InsertPosition::After,
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74struct DropHint {
75 entity: Entity,
76 side: DropSide,
77}
78
79pub trait SegmentedVariant {
81 const VERTICAL: bool;
82
83 fn variant_appearance(
85 theme: &crate::Theme,
86 style: &crate::theme::SegmentedButton,
87 ) -> super::Appearance;
88
89 fn variant_bounds<'b>(
91 &'b self,
92 state: &'b LocalState,
93 bounds: Rectangle,
94 ) -> Box<dyn Iterator<Item = ItemBounds> + 'b>;
95
96 fn variant_layout(
98 &self,
99 state: &mut LocalState,
100 renderer: &crate::Renderer,
101 limits: &layout::Limits,
102 ) -> Size;
103}
104
105#[derive(Setters)]
107#[must_use]
108pub struct SegmentedButton<'a, Variant, SelectionMode, Message>
109where
110 Model<SelectionMode>: Selectable,
111 SelectionMode: Default,
112{
113 #[setters(skip)]
115 pub(super) model: &'a Model<SelectionMode>,
116 pub(super) id: Id,
118 pub(super) close_icon: Icon,
120 pub(super) scrollable_focus: bool,
122 pub(super) show_close_icon_on_hover: bool,
124 #[setters(into)]
126 pub(super) padding: Padding,
127 pub(super) dividers: bool,
129 pub(super) button_alignment: Alignment,
131 pub(super) button_padding: [u16; 4],
133 pub(super) button_height: u16,
135 pub(super) button_spacing: u16,
137 pub(super) maximum_button_width: u16,
139 pub(super) minimum_button_width: u16,
141 pub(super) indent_spacing: u16,
143 pub(super) font_active: crate::font::Font,
145 pub(super) font_hovered: crate::font::Font,
147 pub(super) font_inactive: crate::font::Font,
149 pub(super) font_size: f32,
151 pub(super) width: Length,
153 pub(super) height: Length,
155 pub(super) spacing: u16,
157 pub(super) line_height: LineHeight,
159 pub(super) ellipsize: Ellipsize,
161 #[setters(into)]
163 pub(super) style: Style,
164 #[setters(skip)]
166 pub(super) context_menu: Option<Vec<menu::Tree<Message>>>,
167 #[setters(skip)]
169 pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
170 #[setters(skip)]
171 pub(super) on_close: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
172 #[setters(skip)]
173 pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
174 #[setters(skip)]
175 pub(super) on_middle_press: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
176 #[setters(skip)]
177 pub(super) on_dnd_drop:
178 Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
179 pub(super) mimes: Vec<String>,
180 #[setters(skip)]
181 pub(super) on_dnd_enter: Option<Box<dyn Fn(Entity, Vec<String>) -> Message + 'static>>,
182 #[setters(skip)]
183 pub(super) on_dnd_leave: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
184 #[setters(strip_option)]
185 pub(super) drag_id: Option<DragId>,
186 #[setters(skip)]
187 pub(super) tab_drag: Option<TabDragSource<Message>>,
188 #[setters(skip)]
189 pub(super) on_drop_hint: Option<Box<dyn Fn(Option<(Entity, bool)>) -> Message + 'static>>,
190 #[setters(skip)]
191 pub(super) on_reorder: Option<Box<dyn Fn(ReorderEvent) -> Message + 'static>>,
192 #[setters(skip)]
193 variant: PhantomData<Variant>,
195}
196
197impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message>
198where
199 Self: SegmentedVariant,
200 Model<SelectionMode>: Selectable,
201 SelectionMode: Default,
202{
203 #[inline]
204 pub fn new(model: &'a Model<SelectionMode>) -> Self {
205 Self {
206 model,
207 id: Id::unique(),
208 close_icon: icon::from_name("window-close-symbolic").size(16).icon(),
209 scrollable_focus: false,
210 show_close_icon_on_hover: false,
211 button_alignment: Alignment::Start,
212 padding: Padding::from(0.0),
213 dividers: false,
214 button_padding: [0, 0, 0, 0],
215 button_height: 32,
216 button_spacing: 0,
217 minimum_button_width: u16::MIN,
218 maximum_button_width: u16::MAX,
219 indent_spacing: 16,
220 font_active: crate::font::semibold(),
221 font_hovered: crate::font::default(),
222 font_inactive: crate::font::default(),
223 font_size: 14.0,
224 height: Length::Shrink,
225 width: Length::Fill,
226 spacing: 0,
227 line_height: LineHeight::default(),
228 ellipsize: Ellipsize::default(),
229 style: Style::default(),
230 context_menu: None,
231 on_activate: None,
232 on_close: None,
233 on_context: None,
234 on_middle_press: None,
235 on_dnd_drop: None,
236 on_dnd_enter: None,
237 on_dnd_leave: None,
238 mimes: Vec::new(),
239 variant: PhantomData,
240 drag_id: None,
241 tab_drag: None,
242 on_drop_hint: None,
243 on_reorder: None,
244 }
245 }
246
247 fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) {
248 if let Some(text) = self.model.text.get(key) {
249 let font = if self.button_is_focused(state, key)
250 || state.show_context == Some(key)
251 || self.model.is_active(key)
252 {
253 self.font_active
254 } else if self.button_is_hovered(state, key) {
255 self.font_hovered
256 } else {
257 self.font_inactive
258 };
259
260 let mut hasher = DefaultHasher::new();
261 text.hash(&mut hasher);
262 font.hash(&mut hasher);
263 let text_hash = hasher.finish();
264
265 if let Some(prev_hash) = state.text_hashes.insert(key, text_hash)
266 && prev_hash == text_hash
267 {
268 return;
269 }
270
271 if let Some(paragraph) = state.paragraphs.get_mut(key) {
272 let text = Text {
273 content: text.as_ref(),
274 size: iced::Pixels(self.font_size),
275 bounds: Size::INFINITE,
276 font,
277 align_x: text::Alignment::Left,
278 align_y: alignment::Vertical::Center,
279 shaping: Shaping::Advanced,
280 wrapping: Wrapping::None,
281 line_height: self.line_height,
282 ellipsize: self.ellipsize,
283 };
284 paragraph.update(text);
285 } else {
286 let text = Text {
287 content: text.to_string(),
288 size: iced::Pixels(self.font_size),
289 bounds: Size::INFINITE,
290 font,
291 align_x: text::Alignment::Left,
292 align_y: alignment::Vertical::Center,
293 shaping: Shaping::Advanced,
294 wrapping: Wrapping::None,
295 line_height: self.line_height,
296 ellipsize: self.ellipsize,
297 };
298 state.paragraphs.insert(key, crate::Plain::new(text));
299 }
300 }
301 }
302
303 pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<Message>>>) -> Self
304 where
305 Message: Clone + 'static,
306 {
307 self.context_menu = context_menu.map(|menus| {
308 vec![menu::Tree::with_children(
309 crate::Element::from(crate::widget::Row::new()),
310 menus,
311 )]
312 });
313
314 if let Some(ref mut context_menu) = self.context_menu {
315 context_menu.iter_mut().for_each(menu::Tree::set_index);
316 }
317
318 self
319 }
320
321 pub fn on_activate<T>(mut self, on_activate: T) -> Self
323 where
324 T: Fn(Entity) -> Message + 'static,
325 {
326 self.on_activate = Some(Box::new(on_activate));
327 self
328 }
329
330 pub fn on_close<T>(mut self, on_close: T) -> Self
332 where
333 T: Fn(Entity) -> Message + 'static,
334 {
335 self.on_close = Some(Box::new(on_close));
336 self
337 }
338
339 pub fn on_context<T>(mut self, on_context: T) -> Self
341 where
342 T: Fn(Entity) -> Message + 'static,
343 {
344 self.on_context = Some(Box::new(on_context));
345 self
346 }
347
348 pub fn on_middle_press<T>(mut self, on_middle_press: T) -> Self
350 where
351 T: Fn(Entity) -> Message + 'static,
352 {
353 self.on_middle_press = Some(Box::new(on_middle_press));
354 self
355 }
356
357 pub fn enable_tab_drag(mut self, mime: String) -> Self {
359 self.tab_drag = Some(TabDragSource::new(mime));
360 self
361 }
362
363 pub fn on_drop_hint(
365 mut self,
366 callback: impl Fn(Option<(Entity, bool)>) -> Message + 'static,
367 ) -> Self {
368 self.on_drop_hint = Some(Box::new(callback));
369 self
370 }
371
372 pub fn on_reorder(mut self, callback: impl Fn(ReorderEvent) -> Message + 'static) -> Self {
374 self.on_reorder = Some(Box::new(callback));
375 self
376 }
377
378 pub fn tab_drag_threshold(mut self, threshold: f32) -> Self {
380 if let Some(tab_drag) = self.tab_drag.as_mut() {
381 tab_drag.threshold = threshold.max(1.0);
382 }
383 self
384 }
385
386 fn reorder_event_for_drop(&self, state: &LocalState, target: Entity) -> Option<ReorderEvent> {
387 let dragged = state.dragging_tab?;
388 if dragged == target
389 || !self.model.contains_item(dragged)
390 || !self.model.contains_item(target)
391 {
392 return None;
393 }
394 let position = state
395 .drop_hint
396 .filter(|hint| hint.entity == target)
397 .map(|hint| InsertPosition::from(hint.side))
398 .unwrap_or_else(|| self.default_insert_position(dragged, target));
399 Some(ReorderEvent {
400 dragged,
401 target,
402 position,
403 })
404 }
405
406 fn default_insert_position(&self, dragged: Entity, target: Entity) -> InsertPosition {
407 let len = self.model.len();
408 let target_pos = self
409 .model
410 .position(target)
411 .map(|pos| pos as usize)
412 .unwrap_or(len);
413 let from_pos = self
414 .model
415 .position(dragged)
416 .map(|pos| pos as usize)
417 .unwrap_or(target_pos);
418 if from_pos < target_pos {
419 InsertPosition::After
420 } else {
421 InsertPosition::Before
422 }
423 }
424
425 fn is_enabled(&self, key: Entity) -> bool {
427 self.model.items.get(key).is_some_and(|item| item.enabled)
428 }
429
430 pub fn on_dnd_drop<D: AllowedMimeTypes>(
432 mut self,
433 dnd_drop_handler: impl Fn(Entity, Option<D>, DndAction) -> Message + 'static,
434 ) -> Self {
435 self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| {
436 dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action)
437 }));
438 self.mimes = D::allowed().into_owned();
439 self
440 }
441
442 pub fn on_dnd_enter(
444 mut self,
445 dnd_enter_handler: impl Fn(Entity, Vec<String>) -> Message + 'static,
446 ) -> Self {
447 self.on_dnd_enter = Some(Box::new(dnd_enter_handler));
448 self
449 }
450
451 pub fn on_dnd_leave(mut self, dnd_leave_handler: impl Fn(Entity) -> Message + 'static) -> Self {
453 self.on_dnd_leave = Some(Box::new(dnd_leave_handler));
454 self
455 }
456
457 fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) {
459 match state.focused_item {
460 Item::Tab(entity) => {
461 let mut keys = self.iterate_visible_tabs(state).rev();
462
463 while let Some(key) = keys.next() {
464 if key == entity {
465 for key in keys {
466 if !self.is_enabled(key) {
468 continue;
469 }
470
471 state.focused_item = Item::Tab(key);
472 shell.capture_event();
473 return;
474 }
475
476 break;
477 }
478 }
479
480 if self.prev_tab_sensitive(state) {
481 state.focused_item = Item::PrevButton;
482 shell.capture_event();
483 return;
484 }
485 }
486
487 Item::NextButton => {
488 if let Some(last) = self.last_tab(state) {
489 state.focused_item = Item::Tab(last);
490 shell.capture_event();
491 return;
492 }
493 }
494
495 Item::None => {
496 if self.next_tab_sensitive(state) {
497 state.focused_item = Item::NextButton;
498 shell.capture_event();
499 return;
500 } else if let Some(last) = self.last_tab(state) {
501 state.focused_item = Item::Tab(last);
502 shell.capture_event();
503 return;
504 }
505 }
506
507 Item::PrevButton | Item::Set => (),
508 }
509
510 state.focused_item = Item::None;
511 }
512
513 fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) {
515 match state.focused_item {
516 Item::Tab(entity) => {
517 let mut keys = self.iterate_visible_tabs(state);
518 while let Some(key) = keys.next() {
519 if key == entity {
520 for key in keys {
521 if !self.is_enabled(key) {
523 continue;
524 }
525
526 state.focused_item = Item::Tab(key);
527 shell.capture_event();
528 return;
529 }
530
531 break;
532 }
533 }
534
535 if self.next_tab_sensitive(state) {
536 state.focused_item = Item::NextButton;
537 shell.capture_event();
538 return;
539 }
540 }
541
542 Item::PrevButton => {
543 if let Some(first) = self.first_tab(state) {
544 state.focused_item = Item::Tab(first);
545 shell.capture_event();
546 return;
547 }
548 }
549
550 Item::None => {
551 if self.prev_tab_sensitive(state) {
552 state.focused_item = Item::PrevButton;
553 shell.capture_event();
554 return;
555 } else if let Some(first) = self.first_tab(state) {
556 state.focused_item = Item::Tab(first);
557 shell.capture_event();
558 return;
559 }
560 }
561
562 Item::NextButton | Item::Set => (),
563 }
564
565 state.focused_item = Item::None;
566 }
567
568 fn iterate_visible_tabs<'b>(
569 &'b self,
570 state: &LocalState,
571 ) -> impl DoubleEndedIterator<Item = Entity> + 'b {
572 self.model
573 .order
574 .iter()
575 .copied()
576 .skip(state.buttons_offset)
577 .take(state.buttons_visible)
578 }
579
580 fn first_tab(&self, state: &LocalState) -> Option<Entity> {
581 self.model.order.get(state.buttons_offset).copied()
582 }
583
584 fn last_tab(&self, state: &LocalState) -> Option<Entity> {
585 self.model
586 .order
587 .get(state.buttons_offset + state.buttons_visible)
588 .copied()
589 }
590
591 #[allow(clippy::unused_self)]
592 fn prev_tab_sensitive(&self, state: &LocalState) -> bool {
593 state.buttons_offset > 0
594 }
595
596 fn next_tab_sensitive(&self, state: &LocalState) -> bool {
597 state.buttons_offset < self.model.order.len() - state.buttons_visible
598 }
599
600 pub(super) fn button_dimensions(
601 &self,
602 state: &mut LocalState,
603 font: crate::font::Font,
604 button: Entity,
605 ) -> (f32, f32) {
606 let mut width = 0.0f32;
607 let mut icon_spacing = 0.0f32;
608
609 if let Some((text, entry)) = self
611 .model
612 .text
613 .get(button)
614 .zip(state.paragraphs.entry(button))
615 && !text.is_empty()
616 {
617 icon_spacing = f32::from(self.button_spacing);
618 let paragraph = entry.or_insert_with(|| {
619 crate::Plain::new(Text {
620 content: text.to_string(), size: iced::Pixels(self.font_size),
622 bounds: Size::INFINITE,
623 font,
624 align_x: text::Alignment::Left,
625 align_y: alignment::Vertical::Center,
626 shaping: Shaping::Advanced,
627 wrapping: Wrapping::default(),
628 ellipsize: self.ellipsize,
629 line_height: self.line_height,
630 })
631 });
632
633 let size = paragraph.min_bounds();
634 width += size.width;
635 }
636
637 if let Some(indent) = self.model.indent(button) {
639 width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width);
640 }
641
642 if let Some(icon) = self.model.icon(button) {
644 width += f32::from(icon.size) + icon_spacing;
645 } else if self.model.is_active(button) {
646 if let crate::theme::SegmentedButton::Control = self.style {
648 width += 16.0 + icon_spacing;
649 }
650 }
651
652 if self.model.is_closable(button) {
654 width += f32::from(self.close_icon.size) + f32::from(self.button_spacing);
655 }
656
657 width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]);
659 width = width.min(f32::from(self.maximum_button_width));
660
661 (width, f32::from(self.button_height))
662 }
663
664 pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) {
668 if matches!(self.ellipsize, Ellipsize::None) {
669 return;
670 }
671
672 for (nth, key) in self.model.order.iter().copied().enumerate() {
673 if self.model.text(key).is_some_and(|text| !text.is_empty()) {
674 let mut non_text_width =
675 f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]);
676
677 if let Some(icon) = self.model.icon(key) {
678 non_text_width += f32::from(icon.size) + f32::from(self.button_spacing);
679 } else if self.model.is_active(key) {
680 if let crate::theme::SegmentedButton::Control = self.style {
681 non_text_width += 16.0 + f32::from(self.button_spacing);
682 }
683 }
684
685 if self.model.is_closable(key) {
686 non_text_width +=
687 f32::from(self.close_icon.size) + f32::from(self.button_spacing);
688 }
689
690 let text_width = (available_width - non_text_width).max(0.0);
691
692 if let Some(paragraph) = state.paragraphs.get_mut(key) {
693 paragraph.resize(Size::new(text_width, f32::INFINITY));
694
695 let content_width = paragraph.min_bounds().width + non_text_width
698 - f32::from(self.button_padding[0])
699 - f32::from(self.button_padding[2]);
700 if let Some(entry) = state.internal_layout.get_mut(nth) {
701 entry.1.width = content_width;
702 }
703 }
704 }
705 }
706 }
707
708 pub(super) fn max_button_dimensions(
709 &self,
710 state: &mut LocalState,
711 renderer: &Renderer,
712 ) -> (f32, f32) {
713 let mut width = 0.0f32;
714 let mut height = 0.0f32;
715 let font = renderer.default_font();
716
717 for key in self.model.order.iter().copied() {
718 let (button_width, button_height) = self.button_dimensions(state, font, key);
719
720 state.internal_layout.push((
721 Size::new(button_width, button_height),
722 Size::new(
723 button_width
724 - f32::from(self.button_padding[0])
725 - f32::from(self.button_padding[2]),
726 button_height,
727 ),
728 ));
729
730 height = height.max(button_height);
731 width = width.max(button_width);
732 }
733
734 for (size, actual) in &mut state.internal_layout {
735 size.height = height;
736 actual.height = height;
737 }
738
739 (width, height)
740 }
741
742 fn button_is_focused(&self, state: &LocalState, key: Entity) -> bool {
743 state.focused.is_some()
744 && self.on_activate.is_some()
745 && Item::Tab(key) == state.focused_item
746 }
747
748 fn button_is_hovered(&self, state: &LocalState, key: Entity) -> bool {
749 self.on_activate.is_some() && state.hovered == Item::Tab(key)
750 || state
751 .dnd_state
752 .drag_offer
753 .as_ref()
754 .is_some_and(|id| id.data.is_some_and(|d| d == key))
755 }
756
757 fn button_is_pressed(&self, state: &LocalState, key: Entity) -> bool {
758 state.pressed_item == Some(Item::Tab(key))
759 }
760
761 fn emit_drop_hint(&self, shell: &mut Shell<'_, Message>, hint: Option<DropHint>) {
762 if let Some(on_hint) = self.on_drop_hint.as_ref() {
763 let mapped = hint.map(|hint| (hint.entity, matches!(hint.side, DropSide::After)));
764 shell.publish(on_hint(mapped));
765 }
766 }
767
768 fn drop_hint_for_position(
769 &self,
770 state: &LocalState,
771 bounds: Rectangle,
772 cursor: Point,
773 ) -> Option<DropHint> {
774 let _ = state.dragging_tab?;
775
776 self.variant_bounds(state, bounds)
777 .filter_map(|item| match item {
778 ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)),
779 _ => None,
780 })
781 .map(|(entity, rect)| {
782 let before = if Self::VERTICAL {
783 cursor.y < rect.center_y()
784 } else {
785 cursor.x < rect.center_x()
786 };
787 DropHint {
788 entity,
789 side: if before {
790 DropSide::Before
791 } else {
792 DropSide::After
793 },
794 }
795 })
796 .next()
797 }
798
799 fn start_tab_drag(
800 &self,
801 state: &mut LocalState,
802 entity: Entity,
803 bounds: Rectangle,
804 cursor: Point,
805 clipboard: &mut dyn Clipboard,
806 ) -> bool {
807 let Some(tab_drag) = self.tab_drag.as_ref() else {
808 return false;
809 };
810
811 log::trace!(
812 target: TAB_REORDER_LOG_TARGET,
813 "start_tab_drag requested entity={:?} cursor=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
814 entity,
815 cursor.x,
816 cursor.y,
817 bounds.x,
818 bounds.y,
819 bounds.width,
820 bounds.height,
821 tab_drag.threshold
822 );
823
824 let data_len = 0;
825
826 iced_core::clipboard::start_dnd::<crate::Theme, crate::Renderer>(
827 clipboard,
828 false,
829 Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())),
830 None,
831 Box::new(SimpleDragData::new(tab_drag.mime.clone(), vec![1])),
832 DndAction::Move,
833 );
834 log::trace!(
835 target: TAB_REORDER_LOG_TARGET,
836 "tab drag started entity={:?} mime={} bytes={}",
837 entity,
838 tab_drag.mime,
839 data_len
840 );
841
842 state.dragging_tab = Some(entity);
843 state.tab_drag_candidate = None;
844 state.pressed_item = None;
845 true
846 }
847
848 #[must_use]
853 pub fn get_drag_id(&self) -> u128 {
854 self.drag_id.map_or_else(
855 || {
856 u128::from(match &self.id.0.0 {
857 Internal::Unique(id) | Internal::Custom(id, _) => *id,
858 Internal::Set(_) => panic!("Invalid Id assigned to dnd destination."),
859 })
860 },
861 |id| id.0,
862 )
863 }
864}
865
866impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
867 for SegmentedButton<'_, Variant, SelectionMode, Message>
868where
869 Self: SegmentedVariant,
870 Model<SelectionMode>: Selectable,
871 SelectionMode: Default,
872 Message: 'static + Clone,
873{
874 fn id(&self) -> Option<widget::Id> {
875 Some(self.id.0.clone())
876 }
877
878 fn set_id(&mut self, id: widget::Id) {
879 self.id = Id(id);
880 }
881
882 fn children(&self) -> Vec<Tree> {
883 let mut children = Vec::new();
884
885 if let Some(ref context_menu) = self.context_menu {
887 let mut tree = Tree::empty();
888 tree.state = tree::State::new(MenuBarState::default());
889 tree.children = menu_roots_children(context_menu);
890 children.push(tree);
891 }
892
893 children
894 }
895
896 fn tag(&self) -> tree::Tag {
897 tree::Tag::of::<LocalState>()
898 }
899
900 fn state(&self) -> tree::State {
901 #[allow(clippy::default_trait_access)]
902 tree::State::new(LocalState {
903 menu_state: Default::default(),
904 paragraphs: SecondaryMap::new(),
905 text_hashes: SecondaryMap::new(),
906 buttons_visible: Default::default(),
907 buttons_offset: Default::default(),
908 collapsed: Default::default(),
909 focused: Default::default(),
910 focused_item: Default::default(),
911 focused_visible: false,
912 hovered: Default::default(),
913 known_length: Default::default(),
914 middle_clicked: Default::default(),
915 internal_layout: Default::default(),
916 context_cursor: Point::default(),
917 show_context: Default::default(),
918 wheel_timestamp: Default::default(),
919 dnd_state: Default::default(),
920 fingers_pressed: Default::default(),
921 pressed_item: None,
922 tab_drag_candidate: None,
923 dragging_tab: None,
924 drop_hint: None,
925 offer_mimes: Vec::new(),
926 })
927 }
928
929 fn diff(&mut self, tree: &mut Tree) {
930 let state = tree.state.downcast_mut::<LocalState>();
931 for key in self.model.order.iter().copied() {
932 self.update_entity_paragraph(state, key);
933 }
934
935 if let Some(context_menu) = &mut self.context_menu {
937 state.menu_state.inner.with_data_mut(|inner| {
938 menu_roots_diff(context_menu, &mut inner.tree);
939 });
940 }
941
942 if let Some(f) = state.focused.as_ref()
944 && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get())
945 {
946 state.unfocus();
947 }
948 }
949
950 fn size(&self) -> Size<Length> {
951 Size::new(self.width, self.height)
952 }
953
954 fn layout(
955 &mut self,
956 tree: &mut Tree,
957 renderer: &Renderer,
958 limits: &layout::Limits,
959 ) -> layout::Node {
960 let state = tree.state.downcast_mut::<LocalState>();
961 let limits = limits.shrink(self.padding);
962 let size = self
963 .variant_layout(state, renderer, &limits)
964 .expand(self.padding);
965 layout::Node::new(size)
966 }
967
968 #[allow(clippy::too_many_lines)]
969 fn update(
970 &mut self,
971 tree: &mut Tree,
972 mut event: &Event,
973 layout: Layout<'_>,
974 cursor_position: mouse::Cursor,
975 _renderer: &Renderer,
976 clipboard: &mut dyn Clipboard,
977 shell: &mut Shell<'_, Message>,
978 _viewport: &iced::Rectangle,
979 ) {
980 let my_bounds = layout.bounds();
981 let state = tree.state.downcast_mut::<LocalState>();
982
983 let my_id = self.get_drag_id();
984
985 if let Event::Dnd(e) = &mut event {
986 let entity = state
987 .dnd_state
988 .drag_offer
989 .as_ref()
990 .map(|dnd_state| dnd_state.data);
991 log::trace!(
992 target: TAB_REORDER_LOG_TARGET,
993 "segmented button {:?} received DnD event: {:?} entity={entity:?}",
994 my_id,
995 e
996 );
997 match e {
998 DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => {
999 if state.dragging_tab.take().is_some() {
1000 state.tab_drag_candidate = None;
1001 state.drop_hint = None;
1002 self.emit_drop_hint(shell, state.drop_hint);
1003 log::trace!(
1004 target: TAB_REORDER_LOG_TARGET,
1005 "tab drag source finished id={:?}",
1006 my_id
1007 );
1008 shell.capture_event();
1009 return;
1010 }
1011 }
1012 DndEvent::Offer(
1013 id,
1014 OfferEvent::Enter {
1015 x, y, mime_types, ..
1016 },
1017 ) if Some(my_id) == *id => {
1018 let entity = self
1019 .variant_bounds(state, my_bounds)
1020 .filter_map(|item| match item {
1021 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1022 _ => None,
1023 })
1024 .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1025 .map(|(key, _)| key);
1026 state.drop_hint = self.drop_hint_for_position(
1027 state,
1028 my_bounds,
1029 Point::new(*x as f32, *y as f32),
1030 );
1031 self.emit_drop_hint(shell, state.drop_hint);
1032 log::trace!(
1033 target: TAB_REORDER_LOG_TARGET,
1034 "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}"
1035 );
1036 if let Some(entity) = entity {
1038 state.hovered = Item::Tab(entity);
1039 for key in self.model.order.iter().copied() {
1040 self.update_entity_paragraph(state, key);
1041 }
1042 }
1043
1044 let on_dnd_enter = self
1045 .on_dnd_enter
1046 .as_ref()
1047 .zip(entity)
1048 .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes));
1049 let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime)
1050 && mime_types.is_empty()
1051 {
1052 vec![mime.clone()]
1053 } else {
1054 mime_types.clone()
1055 };
1056 state.offer_mimes.clone_from(&mimes);
1057
1058 _ = state
1059 .dnd_state
1060 .on_enter::<Message>(*x, *y, mimes, on_dnd_enter, entity);
1061 }
1062 DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {}
1063 DndEvent::Offer(id, leave)
1064 if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination)
1065 && Some(my_id) == *id =>
1066 {
1067 state.drop_hint = None;
1068 self.emit_drop_hint(shell, state.drop_hint);
1069 if let Some(Some(entity)) = entity {
1070 if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1071 shell.publish(on_dnd_leave(entity));
1072 }
1073 }
1074 log::trace!(
1075 target: TAB_REORDER_LOG_TARGET,
1076 "offer leave id={my_id:?} entity={entity:?}"
1077 );
1078 state.hovered = Item::None;
1079 for key in self.model.order.iter().copied() {
1080 self.update_entity_paragraph(state, key);
1081 }
1082 _ = state.dnd_state.on_leave::<Message>(None);
1083 }
1084 DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => {
1085 log::trace!(
1086 target: TAB_REORDER_LOG_TARGET,
1087 "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}"
1088 );
1089 let new = self
1090 .variant_bounds(state, my_bounds)
1091 .filter_map(|item| match item {
1092 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1093 _ => None,
1094 })
1095 .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1096 .map(|(key, _)| key);
1097 if let Some(new_entity) = new {
1098 state.dnd_state.on_motion::<Message>(
1099 *x,
1100 *y,
1101 None::<fn(_, _) -> Message>,
1102 None::<fn(_, _, _) -> Message>,
1103 Some(new_entity),
1104 );
1105 state.drop_hint = self.drop_hint_for_position(
1106 state,
1107 my_bounds,
1108 Point::new(*x as f32, *y as f32),
1109 );
1110 self.emit_drop_hint(shell, state.drop_hint);
1111 if Some(Some(new_entity)) != entity {
1112 state.hovered = Item::Tab(new_entity);
1113 for key in self.model.order.iter().copied() {
1114 self.update_entity_paragraph(state, key);
1115 }
1116 let prev_action = state
1117 .dnd_state
1118 .drag_offer
1119 .as_ref()
1120 .map(|dnd| dnd.selected_action);
1121 if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() {
1122 shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone()));
1123 }
1124 if let Some(dnd) = state.dnd_state.drag_offer.as_mut() {
1125 dnd.data = Some(new_entity);
1126 if let Some(prev_action) = prev_action {
1127 dnd.selected_action = prev_action;
1128 }
1129 }
1130 }
1131 } else if entity.is_some() {
1132 state.hovered = Item::None;
1133 for key in self.model.order.iter().copied() {
1134 self.update_entity_paragraph(state, key);
1135 }
1136 log::trace!(
1137 target: TAB_REORDER_LOG_TARGET,
1138 "offer motion leaving id={my_id:?}"
1139 );
1140 state.drop_hint = None;
1141 self.emit_drop_hint(shell, state.drop_hint);
1142 state.dnd_state.on_motion::<Message>(
1143 *x,
1144 *y,
1145 None::<fn(_, _) -> Message>,
1146 None::<fn(_, _, _) -> Message>,
1147 None,
1148 );
1149 if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1150 if let Some(Some(entity)) = entity {
1151 shell.publish(on_dnd_leave(entity));
1152 }
1153 }
1154 }
1155 }
1156 DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => {
1157 log::trace!(
1158 target: TAB_REORDER_LOG_TARGET,
1159 "offer drop id={my_id:?} entity={entity:?}"
1160 );
1161 _ = state
1162 .dnd_state
1163 .on_drop::<Message>(None::<fn(_, _) -> Message>);
1164 }
1165 DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
1166 if state.dnd_state.drag_offer.is_some() {
1167 log::trace!(
1168 target: TAB_REORDER_LOG_TARGET,
1169 "offer selected action id={my_id:?} action={action:?} entity={entity:?}"
1170 );
1171 _ = state
1172 .dnd_state
1173 .on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
1174 }
1175 }
1176 DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
1177 log::trace!(
1178 target: TAB_REORDER_LOG_TARGET,
1179 "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}"
1180 );
1181 let drop_entity = entity
1182 .flatten()
1183 .or_else(|| state.drop_hint.map(|hint| hint.entity));
1184 let allow_reorder = state
1185 .dnd_state
1186 .drag_offer
1187 .as_ref()
1188 .is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
1189 let pending_reorder = if allow_reorder
1190 && self.on_reorder.is_some()
1191 && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
1192 && state.dragging_tab.is_some()
1193 {
1194 drop_entity.and_then(|target| self.reorder_event_for_drop(state, target))
1195 } else {
1196 None
1197 };
1198 if let Some(entity) = drop_entity {
1199 let on_drop = self.on_dnd_drop.as_ref();
1200 let on_drop = on_drop.map(|on_drop| {
1201 |mime, data, action, _, _| on_drop(entity, data, mime, action)
1202 });
1203
1204 let (maybe_msg, ret) = state.dnd_state.on_data_received(
1205 mime_type.clone(),
1206 data.clone(),
1207 None::<fn(_, _) -> Message>,
1208 on_drop,
1209 );
1210 if matches!(ret, iced::event::Status::Captured) {
1211 shell.capture_event();
1212 }
1213 if let Some(msg) = maybe_msg {
1214 log::trace!(
1215 target: TAB_REORDER_LOG_TARGET,
1216 "publishing drop message entity={entity:?}"
1217 );
1218 shell.publish(msg);
1219 }
1220 state.drop_hint = None;
1221
1222 self.emit_drop_hint(shell, state.drop_hint);
1223 if let Some(event) = pending_reorder {
1224 state.focused_item = Item::Tab(event.dragged);
1225 state.hovered = Item::None;
1226 for key in self.model.order.iter().copied() {
1227 self.update_entity_paragraph(state, key);
1228 }
1229 if let Some(on_reorder) = self.on_reorder.as_ref() {
1230 shell.publish(on_reorder(event));
1231 shell.capture_event();
1232 return;
1233 }
1234 }
1235 return;
1236 }
1237 }
1238 _ => {}
1239 }
1240 }
1241
1242 if cursor_position.is_over(my_bounds) {
1243 let fingers_pressed = state.fingers_pressed.len();
1244
1245 match event {
1246 Event::Touch(touch::Event::FingerPressed { id, .. }) => {
1247 state.fingers_pressed.insert(*id);
1248 }
1249
1250 Event::Touch(touch::Event::FingerLifted { id, .. }) => {
1251 state.fingers_pressed.remove(id);
1252 }
1253 _ => (),
1254 }
1255
1256 if state.collapsed {
1258 if cursor_position
1260 .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height)))
1261 && self.prev_tab_sensitive(state)
1262 {
1263 state.hovered = Item::PrevButton;
1264 for key in self.model.order.iter().copied() {
1265 self.update_entity_paragraph(state, key);
1266 }
1267 if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1268 | Event::Touch(touch::Event::FingerLifted { .. }) = event
1269 {
1270 state.buttons_offset -= 1;
1271 }
1272 } else {
1273 if cursor_position
1275 .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height)))
1276 && self.next_tab_sensitive(state)
1277 {
1278 state.hovered = Item::NextButton;
1279 for key in self.model.order.iter().copied() {
1280 self.update_entity_paragraph(state, key);
1281 }
1282 if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1283 | Event::Touch(touch::Event::FingerLifted { .. }) = event
1284 {
1285 state.buttons_offset += 1;
1286 }
1287 }
1288 }
1289 }
1290
1291 for (key, bounds) in self
1292 .variant_bounds(state, my_bounds)
1293 .filter_map(|item| match item {
1294 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1295 _ => None,
1296 })
1297 .collect::<Vec<_>>()
1298 {
1299 if cursor_position.is_over(bounds) {
1300 if self.model.items[key].enabled {
1301 if state.hovered != Item::Tab(key) {
1303 state.hovered = Item::Tab(key);
1304 for key in self.model.order.iter().copied() {
1305 self.update_entity_paragraph(state, key);
1306 }
1307 }
1308
1309 let close_button_bounds =
1310 close_bounds(bounds, f32::from(self.close_icon.size));
1311 let over_close_button = self.model.items[key].closable
1312 && cursor_position.is_over(close_button_bounds);
1313
1314 if self.model.items[key].closable {
1316 if let Some(on_close) = self.on_close.as_ref() {
1318 if over_close_button
1319 && (left_button_released(&event)
1320 || (touch_lifted(&event) && fingers_pressed == 1))
1321 {
1322 shell.publish(on_close(key));
1323 shell.capture_event();
1324 return;
1325 }
1326
1327 if self.on_middle_press.is_none() {
1328 if let Event::Mouse(mouse::Event::ButtonReleased(
1330 mouse::Button::Middle,
1331 )) = event
1332 {
1333 if state.middle_clicked == Some(Item::Tab(key)) {
1334 shell.publish(on_close(key));
1335 shell.capture_event();
1336 return;
1337 }
1338
1339 state.middle_clicked = None;
1340 }
1341 }
1342 }
1343 }
1344
1345 if self.tab_drag.is_some()
1346 && matches!(
1347 event,
1348 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1349 )
1350 && !over_close_button
1351 && let Some(position) = cursor_position.position()
1352 {
1353 state.tab_drag_candidate = Some(TabDragCandidate {
1354 entity: key,
1355 bounds,
1356 origin: position,
1357 });
1358 if let Some(tab_drag) = self.tab_drag.as_ref() {
1359 log::trace!(
1360 target: TAB_REORDER_LOG_TARGET,
1361 "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
1362 key,
1363 position.x,
1364 position.y,
1365 bounds.x,
1366 bounds.y,
1367 bounds.width,
1368 bounds.height,
1369 tab_drag.threshold
1370 );
1371 }
1372 }
1373
1374 if is_lifted(&event) {
1375 state.unfocus();
1376 }
1377
1378 if let Some(on_activate) = self.on_activate.as_ref() {
1379 if is_pressed(event) {
1380 state.pressed_item = Some(Item::Tab(key));
1381 } else if is_lifted(&event) && self.button_is_pressed(state, key) {
1382 shell.publish(on_activate(key));
1383 state.set_focused();
1384 state.focused_item = Item::Tab(key);
1385 state.pressed_item = None;
1386 shell.capture_event();
1387 return;
1388 }
1389 }
1390
1391 if self.context_menu.is_some()
1393 && let Some(on_context) = self.on_context.as_ref()
1394 && (right_button_released(&event)
1395 || (touch_lifted(&event) && fingers_pressed == 2))
1396 {
1397 state.show_context = Some(key);
1398 state.context_cursor = cursor_position.position().unwrap_or_default();
1399
1400 state.menu_state.inner.with_data_mut(|data| {
1401 data.open = true;
1402 data.view_cursor = cursor_position;
1403 });
1404
1405 shell.publish(on_context(key));
1406 shell.capture_event();
1407 return;
1408 }
1409
1410 if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) =
1411 event
1412 {
1413 state.middle_clicked = Some(Item::Tab(key));
1414 if let Some(on_middle_press) = self.on_middle_press.as_ref() {
1415 shell.publish(on_middle_press(key));
1416 shell.capture_event();
1417 return;
1418 }
1419 }
1420 }
1421
1422 break;
1423 } else if state.hovered == Item::Tab(key) {
1424 state.hovered = Item::None;
1425 self.update_entity_paragraph(state, key);
1426 }
1427 }
1428
1429 if self.scrollable_focus
1430 && let Some(on_activate) = self.on_activate.as_ref()
1431 && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
1432 {
1433 let current = Instant::now();
1434
1435 if state.wheel_timestamp.is_none_or(|previous| {
1437 current.duration_since(previous) > Duration::from_millis(250)
1438 }) {
1439 state.wheel_timestamp = Some(current);
1440
1441 match delta {
1442 ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => {
1443 let mut activate_key = None;
1444
1445 if *y < 0.0 {
1446 let mut prev_key = Entity::null();
1447
1448 for key in self.model.order.iter().copied() {
1449 if self.model.is_active(key) && !prev_key.is_null() {
1450 activate_key = Some(prev_key);
1451 }
1452
1453 if self.model.is_enabled(key) {
1454 prev_key = key;
1455 }
1456 }
1457 } else if *y > 0.0 {
1458 let mut buttons = self.model.order.iter().copied();
1459 while let Some(key) = buttons.next() {
1460 if self.model.is_active(key) {
1461 for key in buttons {
1462 if self.model.is_enabled(key) {
1463 activate_key = Some(key);
1464 break;
1465 }
1466 }
1467 break;
1468 }
1469 }
1470 }
1471
1472 if let Some(key) = activate_key {
1473 shell.publish(on_activate(key));
1474 state.set_focused();
1475 state.focused_item = Item::Tab(key);
1476 shell.capture_event();
1477 return;
1478 }
1479 }
1480 }
1481 }
1482 }
1483 } else {
1484 if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) {
1485 for key in self.model.order.iter().copied() {
1486 self.update_entity_paragraph(state, key);
1487 }
1488 }
1489 if state.is_focused() {
1490 if is_pressed(&event) {
1492 state.unfocus();
1493 state.pressed_item = None;
1494 return;
1495 }
1496 } else if is_lifted(&event) {
1497 state.pressed_item = None;
1498 }
1499 }
1500
1501 if let (Some(tab_drag), Some(candidate)) =
1502 (self.tab_drag.as_ref(), state.tab_drag_candidate)
1503 && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
1504 && let Some(position) = cursor_position.position()
1505 && position.distance(candidate.origin) >= tab_drag.threshold
1506 && let Some(candidate) = state.tab_drag_candidate.take()
1507 {
1508 log::trace!(
1509 target: TAB_REORDER_LOG_TARGET,
1510 "tab drag threshold met entity={:?} distance={:.2} threshold={}",
1511 candidate.entity,
1512 position.distance(candidate.origin),
1513 tab_drag.threshold
1514 );
1515 if self.start_tab_drag(
1516 state,
1517 candidate.entity,
1518 candidate.bounds,
1519 position,
1520 clipboard,
1521 ) {
1522 shell.capture_event();
1523 return;
1524 }
1525 }
1526
1527 if matches!(
1528 event,
1529 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1530 ) {
1531 state.tab_drag_candidate = None;
1532 }
1533
1534 if state.is_focused() {
1535 if let Event::Keyboard(keyboard::Event::KeyPressed {
1536 key: keyboard::Key::Named(keyboard::key::Named::Tab),
1537 modifiers,
1538 ..
1539 }) = event
1540 {
1541 state.focused_visible = true;
1542 return if *modifiers == keyboard::Modifiers::SHIFT {
1543 self.focus_previous(state, shell);
1544 } else if modifiers.is_empty() {
1545 self.focus_next(state, shell);
1546 };
1547 }
1548
1549 if let Some(on_activate) = self.on_activate.as_ref()
1550 && let Event::Keyboard(keyboard::Event::KeyReleased {
1551 key: keyboard::Key::Named(keyboard::key::Named::Enter),
1552 ..
1553 }) = event
1554 {
1555 match state.focused_item {
1556 Item::Tab(entity) => {
1557 shell.publish(on_activate(entity));
1558 }
1559
1560 Item::PrevButton => {
1561 if self.prev_tab_sensitive(state) {
1562 state.buttons_offset -= 1;
1563
1564 if !self.prev_tab_sensitive(state)
1566 && let Some(first) = self.first_tab(state)
1567 {
1568 state.focused_item = Item::Tab(first);
1569 }
1570 }
1571 }
1572
1573 Item::NextButton => {
1574 if self.next_tab_sensitive(state) {
1575 state.buttons_offset += 1;
1576
1577 if !self.next_tab_sensitive(state)
1579 && let Some(last) = self.last_tab(state)
1580 {
1581 state.focused_item = Item::Tab(last);
1582 }
1583 }
1584 }
1585
1586 Item::None | Item::Set => (),
1587 }
1588
1589 shell.capture_event();
1590 }
1591 }
1592 }
1593
1594 fn operate(
1595 &mut self,
1596 tree: &mut Tree,
1597 layout: Layout<'_>,
1598 _renderer: &Renderer,
1599 operation: &mut dyn iced_core::widget::Operation<()>,
1600 ) {
1601 let state = tree.state.downcast_mut::<LocalState>();
1602 operation.focusable(Some(&self.id.0), layout.bounds(), state);
1603 operation.custom(Some(&self.id.0), layout.bounds(), state);
1604
1605 if let Item::Set = state.focused_item {
1606 if self.prev_tab_sensitive(state) {
1607 state.focused_item = Item::PrevButton;
1608 } else if let Some(first) = self.first_tab(state) {
1609 state.focused_item = Item::Tab(first);
1610 }
1611 }
1612 }
1613
1614 fn mouse_interaction(
1615 &self,
1616 tree: &Tree,
1617 layout: Layout<'_>,
1618 cursor_position: mouse::Cursor,
1619 _viewport: &iced::Rectangle,
1620 _renderer: &Renderer,
1621 ) -> iced_core::mouse::Interaction {
1622 if self.on_activate.is_none() {
1623 return iced_core::mouse::Interaction::default();
1624 }
1625 let state = tree.state.downcast_ref::<LocalState>();
1626 let bounds = layout.bounds();
1627
1628 if cursor_position.is_over(bounds) {
1629 let hovered_button = self
1630 .variant_bounds(state, bounds)
1631 .filter_map(|item| match item {
1632 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1633 _ => None,
1634 })
1635 .find(|(_key, bounds)| cursor_position.is_over(*bounds));
1636
1637 if let Some((key, _bounds)) = hovered_button {
1638 return if self.model.items[key].enabled {
1639 iced_core::mouse::Interaction::Pointer
1640 } else {
1641 iced_core::mouse::Interaction::Idle
1642 };
1643 }
1644 }
1645
1646 iced_core::mouse::Interaction::default()
1647 }
1648
1649 #[allow(clippy::too_many_lines)]
1650 fn draw(
1651 &self,
1652 tree: &Tree,
1653 renderer: &mut Renderer,
1654 theme: &crate::Theme,
1655 style: &renderer::Style,
1656 layout: Layout<'_>,
1657 cursor: mouse::Cursor,
1658 viewport: &iced::Rectangle,
1659 ) {
1660 let state = tree.state.downcast_ref::<LocalState>();
1661 let appearance = Self::variant_appearance(theme, &self.style);
1662 let bounds: Rectangle = layout.bounds();
1663 let button_amount = self.model.items.len();
1664 let show_drop_hint = state.dragging_tab.is_some();
1665 let drop_hint = if show_drop_hint {
1666 state.drop_hint
1667 } else {
1668 None
1669 };
1670
1671 if let Some(background) = appearance.background {
1673 renderer.fill_quad(
1674 renderer::Quad {
1675 bounds,
1676 border: appearance.border,
1677 shadow: Shadow::default(),
1678 snap: true,
1679 },
1680 background,
1681 );
1682 }
1683
1684 if state.collapsed {
1686 let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height));
1687
1688 let mut background_appearance =
1690 if self.on_activate.is_some() && Item::PrevButton == state.focused_item {
1691 Some(appearance.active)
1692 } else if self.on_activate.is_some() && Item::PrevButton == state.hovered {
1693 Some(appearance.hover)
1694 } else {
1695 None
1696 };
1697
1698 if let Some(background_appearance) = background_appearance.take() {
1699 renderer.fill_quad(
1700 renderer::Quad {
1701 bounds: tab_bounds,
1702 border: Border {
1703 radius: theme.cosmic().radius_s().into(),
1704 ..Default::default()
1705 },
1706 shadow: Shadow::default(),
1707 snap: true,
1708 },
1709 background_appearance
1710 .background
1711 .unwrap_or(Background::Color(Color::TRANSPARENT)),
1712 );
1713 }
1714
1715 draw_icon::<Message>(
1716 renderer,
1717 theme,
1718 style,
1719 cursor,
1720 viewport,
1721 if state.buttons_offset == 0 {
1722 appearance.inactive.text_color
1723 } else {
1724 appearance.active.text_color
1725 },
1726 Rectangle {
1727 x: tab_bounds.x + 8.0,
1728 y: tab_bounds.y + f32::from(self.button_height) / 4.0,
1729 width: 16.0,
1730 height: 16.0,
1731 },
1732 icon::from_name("go-previous-symbolic").size(16).icon(),
1733 );
1734
1735 tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height));
1736
1737 background_appearance =
1739 if self.on_activate.is_some() && Item::NextButton == state.focused_item {
1740 Some(appearance.active)
1741 } else if self.on_activate.is_some() && Item::NextButton == state.hovered {
1742 Some(appearance.hover)
1743 } else {
1744 None
1745 };
1746
1747 if let Some(background_appearance) = background_appearance {
1748 renderer.fill_quad(
1749 renderer::Quad {
1750 bounds: tab_bounds,
1751 border: Border {
1752 radius: theme.cosmic().radius_s().into(),
1753 ..Default::default()
1754 },
1755 shadow: Shadow::default(),
1756 snap: true,
1757 },
1758 background_appearance
1759 .background
1760 .unwrap_or(Background::Color(Color::TRANSPARENT)),
1761 );
1762 }
1763
1764 draw_icon::<Message>(
1765 renderer,
1766 theme,
1767 style,
1768 cursor,
1769 viewport,
1770 if self.next_tab_sensitive(state) {
1771 appearance.active.text_color
1772 } else if let Item::NextButton = state.focused_item {
1773 appearance.active.text_color
1774 } else {
1775 appearance.inactive.text_color
1776 },
1777 Rectangle {
1778 x: tab_bounds.x + 8.0,
1779 y: tab_bounds.y + f32::from(self.button_height) / 4.0,
1780 width: 16.0,
1781 height: 16.0,
1782 },
1783 icon::from_name("go-next-symbolic").size(16).icon(),
1784 );
1785 }
1786
1787 let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
1788
1789 let divider_background = Background::Color(
1790 crate::theme::active()
1791 .cosmic()
1792 .primary_component_divider()
1793 .into(),
1794 );
1795
1796 let mut nth = 0;
1798 let drop_hint_marker = drop_hint;
1799 let show_drop_hint_marker = show_drop_hint;
1800 self.variant_bounds(state, bounds).for_each(move |item| {
1801 let (key, mut bounds) = match item {
1802 ItemBounds::Button(entity, bounds) => (entity, bounds),
1804
1805 ItemBounds::Divider(bounds, accented) => {
1807 renderer.fill_quad(
1808 renderer::Quad {
1809 bounds,
1810 border: Border::default(),
1811 shadow: Shadow::default(),
1812 snap: true,
1813 },
1814 {
1815 let theme = crate::theme::active();
1816 if accented {
1817 Background::Color(theme.cosmic().small_widget_divider().into())
1818 } else {
1819 Background::Color(theme.cosmic().primary_container_divider().into())
1820 }
1821 },
1822 );
1823
1824 return;
1825 }
1826 };
1827
1828 let original_bounds = bounds;
1829 let center_y = bounds.center_y();
1830
1831 if show_drop_hint_marker
1832 && matches!(
1833 drop_hint_marker,
1834 Some(DropHint {
1835 entity,
1836 side: DropSide::Before
1837 }) if entity == key
1838 )
1839 {
1840 draw_drop_indicator(
1841 renderer,
1842 original_bounds,
1843 DropSide::Before,
1844 Self::VERTICAL,
1845 appearance.active.text_color,
1846 );
1847 }
1848
1849 let menu_open = || {
1850 state.show_context == Some(key)
1851 && !tree.children.is_empty()
1852 && tree.children[0]
1853 .state
1854 .downcast_ref::<MenuBarState>()
1855 .inner
1856 .with_data(|data| data.open)
1857 };
1858
1859 let key_is_active = self.model.is_active(key);
1860 let key_is_focused = state.focused_visible && self.button_is_focused(state, key);
1861 let key_is_hovered = self.button_is_hovered(state, key);
1862 let status_appearance = if self.button_is_pressed(state, key) {
1863 appearance.pressed
1864 } else if key_is_hovered || menu_open() {
1865 appearance.hover
1866 } else if key_is_active {
1867 appearance.active
1868 } else {
1869 appearance.inactive
1870 };
1871
1872 let button_appearance = if nth == 0 {
1873 status_appearance.first
1874 } else if nth + 1 == button_amount {
1875 status_appearance.last
1876 } else {
1877 status_appearance.middle
1878 };
1879
1880 if appearance.active_width > 0.0 {
1882 let active_width = if key_is_active {
1883 appearance.active_width
1884 } else {
1885 1.0
1886 };
1887
1888 renderer.fill_quad(
1889 renderer::Quad {
1890 bounds: if Self::VERTICAL {
1891 Rectangle {
1892 x: bounds.x + bounds.width - active_width,
1893 width: active_width,
1894 ..bounds
1895 }
1896 } else {
1897 Rectangle {
1898 y: bounds.y + bounds.height - active_width,
1899 height: active_width,
1900 ..bounds
1901 }
1902 },
1903 border: Border {
1904 radius: rad_0.into(),
1905 ..Default::default()
1906 },
1907 shadow: Shadow::default(),
1908 snap: true,
1909 },
1910 appearance.active.text_color,
1911 );
1912 }
1913
1914 bounds.x += f32::from(self.button_padding[0]);
1915 bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
1916 let mut indent_padding = 0.0;
1917
1918 if let Some(indent) = self.model.indent(key)
1920 && indent > 0
1921 {
1922 let adjustment = f32::from(indent) * f32::from(self.indent_spacing);
1923 bounds.x += adjustment;
1924 bounds.width -= adjustment;
1925
1926 if let crate::theme::SegmentedButton::FileNav = self.style
1928 && indent > 1
1929 {
1930 indent_padding = 7.0;
1931
1932 for level in 1..indent {
1933 renderer.fill_quad(
1934 renderer::Quad {
1935 bounds: Rectangle {
1936 x: (level as f32)
1937 .mul_add(-(self.indent_spacing as f32), bounds.x)
1938 + indent_padding,
1939 width: 1.0,
1940 ..bounds
1941 },
1942 border: Border {
1943 radius: rad_0.into(),
1944 ..Default::default()
1945 },
1946 shadow: Shadow::default(),
1947 snap: true,
1948 },
1949 divider_background,
1950 );
1951 }
1952
1953 indent_padding += 4.0;
1954 }
1955 }
1956
1957 if key_is_focused || status_appearance.background.is_some() {
1959 renderer.fill_quad(
1960 renderer::Quad {
1961 bounds: Rectangle {
1962 x: bounds.x - f32::from(self.button_padding[0]) + indent_padding,
1963 width: bounds.width + f32::from(self.button_padding[0])
1964 - f32::from(self.button_padding[2])
1965 - indent_padding,
1966 ..bounds
1967 },
1968 border: if key_is_focused {
1969 Border {
1970 width: 1.0,
1971 color: appearance.active.text_color,
1972 radius: button_appearance.border.radius,
1973 }
1974 } else {
1975 button_appearance.border
1976 },
1977 shadow: Shadow::default(),
1978 snap: true,
1979 },
1980 status_appearance
1981 .background
1982 .unwrap_or(Background::Color(Color::TRANSPARENT)),
1983 );
1984 }
1985
1986 {
1988 let actual_width = state.internal_layout[nth].1.width.min(bounds.width);
1991
1992 let offset = match self.button_alignment {
1993 Alignment::Start => None,
1994 Alignment::Center => Some((bounds.width - actual_width) / 2.0),
1995 Alignment::End => Some(bounds.width - actual_width),
1996 };
1997
1998 if let Some(offset) = offset {
1999 bounds.x += offset - f32::from(self.button_padding[0]);
2000 bounds.width = actual_width;
2001 }
2002 }
2003
2004 if let Some(icon) = self.model.icon(key) {
2006 let mut image_bounds = bounds;
2007 let width = f32::from(icon.size);
2008 let offset = width + f32::from(self.button_spacing);
2009 image_bounds.y = center_y - width / 2.0;
2010
2011 draw_icon::<Message>(
2012 renderer,
2013 theme,
2014 style,
2015 cursor,
2016 viewport,
2017 status_appearance.text_color,
2018 Rectangle {
2019 width,
2020 height: width,
2021 ..image_bounds
2022 },
2023 icon.clone(),
2024 );
2025
2026 bounds.x += offset;
2027 } else {
2028 if key_is_active && let crate::theme::SegmentedButton::Control = self.style {
2030 let mut image_bounds = bounds;
2031 image_bounds.y = center_y - 8.0;
2032
2033 draw_icon::<Message>(
2034 renderer,
2035 theme,
2036 style,
2037 cursor,
2038 viewport,
2039 status_appearance.text_color,
2040 Rectangle {
2041 width: 16.0,
2042 height: 16.0,
2043 ..image_bounds
2044 },
2045 crate::widget::icon(match crate::widget::common::object_select().data() {
2046 iced_core::svg::Data::Bytes(bytes) => {
2047 crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true)
2048 }
2049 iced_core::svg::Data::Path(path) => {
2050 crate::widget::icon::from_path(path.clone())
2051 }
2052 }),
2053 );
2054
2055 let offset = 16.0 + f32::from(self.button_spacing);
2056
2057 bounds.x += offset;
2058 }
2059 }
2060
2061 let show_close_button =
2063 (key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
2064 && self.model.is_closable(key);
2065
2066 let close_icon_width = if show_close_button {
2068 f32::from(self.close_icon.size)
2069 } else {
2070 0.0
2071 };
2072
2073 bounds.width = original_bounds.width
2074 - (bounds.x - original_bounds.x)
2075 - close_icon_width
2076 - f32::from(self.button_padding[2]);
2077
2078 bounds.y = center_y;
2079
2080 if self.model.text(key).is_some_and(|text| !text.is_empty()) {
2081 bounds.y -= state.paragraphs[key].min_height() / 2.;
2083
2084 renderer.fill_paragraph(
2086 state.paragraphs[key].raw(),
2087 bounds.position(),
2088 status_appearance.text_color,
2089 Rectangle {
2090 x: bounds.x,
2091 width: bounds.width,
2092 height: original_bounds.height,
2093 y: bounds.y,
2094 },
2096 );
2097 }
2098
2099 if show_close_button {
2101 let close_button_bounds = close_bounds(original_bounds, close_icon_width);
2102
2103 draw_icon::<Message>(
2104 renderer,
2105 theme,
2106 style,
2107 cursor,
2108 viewport,
2109 status_appearance.text_color,
2110 close_button_bounds,
2111 self.close_icon.clone(),
2112 );
2113 }
2114
2115 if show_drop_hint_marker {
2116 if matches!(
2117 drop_hint_marker,
2118 Some(DropHint {
2119 entity,
2120 side: DropSide::After
2121 }) if entity == key
2122 ) {
2123 draw_drop_indicator(
2124 renderer,
2125 original_bounds,
2126 DropSide::After,
2127 Self::VERTICAL,
2128 appearance.active.text_color,
2129 );
2130 }
2131 }
2132
2133 nth += 1;
2134 });
2135 }
2136
2137 fn overlay<'b>(
2138 &'b mut self,
2139 tree: &'b mut Tree,
2140 layout: iced_core::Layout<'b>,
2141 _renderer: &Renderer,
2142 _viewport: &iced_core::Rectangle,
2143 translation: Vector,
2144 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
2145 let state = tree.state.downcast_mut::<LocalState>();
2146 let menu_state = state.menu_state.clone();
2147
2148 let entity = state.show_context?;
2149
2150 let mut bounds =
2151 self.variant_bounds(state, layout.bounds())
2152 .find_map(|item| match item {
2153 ItemBounds::Button(e, bounds) if e == entity => Some(bounds),
2154 _ => None,
2155 })?;
2156
2157 let context_menu = self.context_menu.as_mut()?;
2158
2159 if !menu_state.inner.with_data(|data| data.open) {
2160 state.show_context = None;
2164 for key in self.model.order.iter().copied() {
2165 self.update_entity_paragraph(state, key);
2166 }
2167 return None;
2168 }
2169 bounds.x = state.context_cursor.x;
2170 bounds.y = state.context_cursor.y;
2171
2172 Some(
2173 crate::widget::menu::Menu {
2174 tree: menu_state,
2175 menu_roots: std::borrow::Cow::Owned(context_menu.clone()),
2176 bounds_expand: 16,
2177 menu_overlays_parent: true,
2178 close_condition: CloseCondition {
2179 leave: false,
2180 click_outside: true,
2181 click_inside: true,
2182 },
2183 item_width: ItemWidth::Uniform(240),
2184 item_height: ItemHeight::Dynamic(40),
2185 bar_bounds: bounds,
2186 main_offset: -bounds.height as i32,
2187 cross_offset: 0,
2188 root_bounds_list: vec![bounds],
2189 path_highlight: Some(PathHighlight::MenuActive),
2190 style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
2191 position: Point::new(translation.x, translation.y),
2192 is_overlay: true,
2193 window_id: window::Id::NONE,
2194 depth: 0,
2195 on_surface_action: None,
2196 }
2197 .overlay(),
2198 )
2199 }
2200
2201 fn drag_destinations(
2202 &self,
2203 tree: &Tree,
2204 layout: Layout<'_>,
2205 _renderer: &Renderer,
2206 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
2207 ) {
2208 let local_state = tree.state.downcast_ref::<LocalState>();
2209 let my_id = self.get_drag_id();
2210 let mut pushed = false;
2211
2212 for item in self.variant_bounds(local_state, layout.bounds()) {
2213 if let ItemBounds::Button(_entity, rect) = item {
2214 pushed = true;
2215 log::trace!(
2216 target: TAB_REORDER_LOG_TARGET,
2217 "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2218 my_id,
2219 rect.x,
2220 rect.y,
2221 rect.width,
2222 rect.height,
2223 self.mimes
2224 );
2225 dnd_rectangles.push(DndDestinationRectangle {
2226 id: my_id,
2227 rectangle: dnd::Rectangle {
2228 x: f64::from(rect.x),
2229 y: f64::from(rect.y),
2230 width: f64::from(rect.width),
2231 height: f64::from(rect.height),
2232 },
2233 mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2234 actions: DndAction::Copy | DndAction::Move,
2235 preferred: DndAction::Move,
2236 });
2237 }
2238 }
2239
2240 if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) {
2241 for item in self.variant_bounds(local_state, layout.bounds()) {
2242 if let ItemBounds::Button(_entity, rect) = item {
2243 pushed = true;
2244 log::trace!(
2245 target: TAB_REORDER_LOG_TARGET,
2246 "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2247 my_id,
2248 rect.x,
2249 rect.y,
2250 rect.width,
2251 rect.height,
2252 mime
2253 );
2254 dnd_rectangles.push(DndDestinationRectangle {
2255 id: my_id,
2256 rectangle: dnd::Rectangle {
2257 x: f64::from(rect.x),
2258 y: f64::from(rect.y),
2259 width: f64::from(rect.width),
2260 height: f64::from(rect.height),
2261 },
2262 mime_types: vec![Cow::Owned(mime.clone())],
2263 actions: DndAction::Copy | DndAction::Move,
2264 preferred: DndAction::Move,
2265 });
2266 }
2267 }
2268 }
2269
2270 if !pushed {
2271 let bounds = layout.bounds();
2272 log::trace!(
2273 target: TAB_REORDER_LOG_TARGET,
2274 "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2275 my_id,
2276 bounds.x,
2277 bounds.y,
2278 bounds.width,
2279 bounds.height,
2280 self.mimes
2281 );
2282 dnd_rectangles.push(DndDestinationRectangle {
2283 id: my_id,
2284 rectangle: dnd::Rectangle {
2285 x: f64::from(bounds.x),
2286 y: f64::from(bounds.y),
2287 width: f64::from(bounds.width),
2288 height: f64::from(bounds.height),
2289 },
2290 mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2291 actions: DndAction::Copy | DndAction::Move,
2292 preferred: DndAction::Move,
2293 });
2294 }
2295 }
2296}
2297
2298impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
2299 for Element<'a, Message>
2300where
2301 SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
2302 Variant: 'static,
2303 Model<SelectionMode>: Selectable,
2304 SelectionMode: Default,
2305 Message: 'static + Clone,
2306{
2307 fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self {
2308 if widget.model.items.is_empty() {
2309 widget.spacing = 0;
2310 }
2311
2312 Self::new(widget)
2313 }
2314}
2315
2316struct TabDragSource<Message> {
2317 mime: String,
2318 threshold: f32,
2319 _marker: PhantomData<Message>,
2320}
2321
2322impl<Message> TabDragSource<Message> {
2323 fn new(mime: String) -> Self {
2324 Self {
2325 mime,
2326 threshold: 8.0,
2327 _marker: PhantomData,
2328 }
2329 }
2330}
2331
2332struct SimpleDragData {
2333 mime: String,
2334 bytes: Vec<u8>,
2335}
2336
2337impl SimpleDragData {
2338 fn new(mime: String, bytes: Vec<u8>) -> Self {
2339 Self { mime, bytes }
2340 }
2341}
2342
2343impl iced::clipboard::mime::AsMimeTypes for SimpleDragData {
2344 fn available(&self) -> Cow<'static, [String]> {
2345 Cow::Owned(vec![self.mime.clone()])
2346 }
2347
2348 fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
2349 if mime_type == self.mime {
2350 Some(Cow::Owned(self.bytes.clone()))
2351 } else {
2352 None
2353 }
2354 }
2355}
2356
2357#[derive(Clone, Copy)]
2358struct TabDragCandidate {
2359 entity: Entity,
2360 bounds: Rectangle,
2361 origin: Point,
2362}
2363
2364#[derive(Debug, Clone, Copy)]
2365struct Focus {
2366 updated_at: Instant,
2367 now: Instant,
2368}
2369
2370pub struct LocalState {
2372 pub(crate) menu_state: MenuBarState,
2374 pub(super) buttons_visible: usize,
2376 pub(super) buttons_offset: usize,
2378 pub(super) collapsed: bool,
2380 focused_visible: bool,
2382 focused: Option<Focus>,
2384 focused_item: Item,
2386 hovered: Item,
2388 middle_clicked: Option<Item>,
2390 pub(super) known_length: usize,
2392 pub(super) internal_layout: Vec<(Size, Size)>,
2394 paragraphs: SecondaryMap<Entity, crate::Plain>,
2396 text_hashes: SecondaryMap<Entity, u64>,
2398 context_cursor: Point,
2400 show_context: Option<Entity>,
2402 wheel_timestamp: Option<Instant>,
2404 pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
2406 pub offer_mimes: Vec<String>,
2408 fingers_pressed: HashSet<Finger>,
2410 pressed_item: Option<Item>,
2412 tab_drag_candidate: Option<TabDragCandidate>,
2414 dragging_tab: Option<Entity>,
2416 drop_hint: Option<DropHint>,
2418}
2419
2420#[derive(Debug, Default, PartialEq)]
2421enum Item {
2422 NextButton,
2423 #[default]
2424 None,
2425 PrevButton,
2426 Set,
2427 Tab(Entity),
2428}
2429
2430impl LocalState {
2431 fn set_focused(&mut self) {
2432 let now = Instant::now();
2433 LAST_FOCUS_UPDATE.with(|x| x.set(now));
2434
2435 self.focused = Some(Focus {
2436 updated_at: now,
2437 now,
2438 });
2439 }
2440}
2441
2442#[cfg(test)]
2443mod tests {
2444 use super::*;
2445 use crate::widget::segmented_button::{self, Appearance as SegAppearance};
2446 use iced::Size;
2447 use slotmap::SecondaryMap;
2448 use std::collections::HashSet;
2449
2450 #[derive(Clone, Debug)]
2451 enum TestMessage {}
2452
2453 struct TestVariant;
2454
2455 impl<SelectionMode, Message> SegmentedVariant
2456 for SegmentedButton<'_, TestVariant, SelectionMode, Message>
2457 where
2458 Model<SelectionMode>: Selectable,
2459 SelectionMode: Default,
2460 {
2461 const VERTICAL: bool = false;
2462
2463 fn variant_appearance(
2464 _theme: &crate::Theme,
2465 _style: &crate::theme::SegmentedButton,
2466 ) -> SegAppearance {
2467 SegAppearance::default()
2468 }
2469
2470 fn variant_bounds<'b>(
2471 &'b self,
2472 _state: &'b LocalState,
2473 bounds: Rectangle,
2474 ) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
2475 let len = self.model.order.len();
2476 if len == 0 {
2477 return Box::new(std::iter::empty());
2478 }
2479 let width = bounds.width / len as f32;
2480 Box::new(
2481 self.model
2482 .order
2483 .iter()
2484 .copied()
2485 .enumerate()
2486 .map(move |(idx, entity)| {
2487 let rect = Rectangle {
2488 x: bounds.x + (idx as f32) * width,
2489 y: bounds.y,
2490 width,
2491 height: bounds.height,
2492 };
2493 ItemBounds::Button(entity, rect)
2494 }),
2495 )
2496 }
2497
2498 fn variant_layout(
2499 &self,
2500 _state: &mut LocalState,
2501 _renderer: &crate::Renderer,
2502 _limits: &layout::Limits,
2503 ) -> Size {
2504 Size::ZERO
2505 }
2506 }
2507
2508 fn sample_model() -> (
2509 segmented_button::SingleSelectModel,
2510 Vec<segmented_button::Entity>,
2511 ) {
2512 let mut entities = Vec::new();
2513 let model = segmented_button::Model::builder()
2514 .insert(|b| b.text("One").with_id(|id| entities.push(id)))
2515 .insert(|b| b.text("Two").with_id(|id| entities.push(id)))
2516 .insert(|b| b.text("Three").with_id(|id| entities.push(id)))
2517 .build();
2518 (model, entities)
2519 }
2520
2521 fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState {
2522 let mut state = LocalState {
2523 menu_state: MenuBarState::default(),
2524 paragraphs: SecondaryMap::new(),
2525 text_hashes: SecondaryMap::new(),
2526 buttons_visible: 0,
2527 buttons_offset: 0,
2528 collapsed: false,
2529 focused: None,
2530 focused_item: Item::default(),
2531 focused_visible: false,
2532 hovered: Item::default(),
2533 known_length: 0,
2534 middle_clicked: None,
2535 internal_layout: Vec::new(),
2536 context_cursor: Point::ORIGIN,
2537 show_context: None,
2538 wheel_timestamp: None,
2539 dnd_state: crate::widget::dnd_destination::State::<Option<Entity>>::new(),
2540 fingers_pressed: HashSet::new(),
2541 pressed_item: None,
2542 tab_drag_candidate: None,
2543 dragging_tab: Some(dragging),
2544 drop_hint: None,
2545 offer_mimes: Vec::new(),
2546 };
2547 state.buttons_visible = len;
2548 state.known_length = len;
2549 state
2550 }
2551
2552 #[test]
2553 fn drop_hint_reports_before_and_after() {
2554 let (model, ids) = sample_model();
2555 let button =
2556 SegmentedButton::<TestVariant, segmented_button::SingleSelect, TestMessage>::new(
2557 &model,
2558 );
2559 let state = test_state(ids[0], model.order.len());
2560 let bounds = Rectangle {
2561 x: 0.0,
2562 y: 0.0,
2563 width: 300.0,
2564 height: 30.0,
2565 };
2566 let before = button
2567 .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0))
2568 .expect("hint");
2569 assert_eq!(before.entity, ids[0]);
2570 assert!(matches!(before.side, DropSide::Before));
2571
2572 let after = button
2573 .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0))
2574 .expect("hint");
2575 assert_eq!(after.entity, ids[2]);
2576 assert!(matches!(after.side, DropSide::After));
2577 }
2578}
2579
2580impl operation::Focusable for LocalState {
2581 fn is_focused(&self) -> bool {
2582 self.focused
2583 .is_some_and(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()))
2584 }
2585
2586 fn focus(&mut self) {
2587 self.set_focused();
2588 self.focused_visible = true;
2589 self.focused_item = Item::Set;
2590 }
2591
2592 fn unfocus(&mut self) {
2593 self.focused = None;
2594 self.focused_item = Item::None;
2595 self.focused_visible = false;
2596 self.show_context = None;
2597 }
2598}
2599
2600#[derive(Debug, Clone, PartialEq)]
2602pub struct Id(widget::Id);
2603
2604impl Id {
2605 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
2607 Self(widget::Id::new(id))
2608 }
2609
2610 #[must_use]
2614 #[inline]
2615 pub fn unique() -> Self {
2616 Self(widget::Id::unique())
2617 }
2618}
2619
2620impl From<Id> for widget::Id {
2621 fn from(id: Id) -> Self {
2622 id.0
2623 }
2624}
2625
2626fn close_bounds(area: Rectangle<f32>, icon_size: f32) -> Rectangle<f32> {
2628 Rectangle {
2629 x: area.x + area.width - icon_size - 8.0,
2630 y: area.center_y() - (icon_size / 2.0),
2631 width: icon_size,
2632 height: icon_size,
2633 }
2634}
2635
2636fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
2638 Rectangle {
2639 x: bounds.x + bounds.width - button_height,
2640 y: bounds.y,
2641 width: button_height,
2642 height: button_height,
2643 }
2644}
2645
2646fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
2648 Rectangle {
2649 x: bounds.x,
2650 y: bounds.y,
2651 width: button_height,
2652 height: button_height,
2653 }
2654}
2655
2656#[allow(clippy::too_many_arguments)]
2657fn draw_icon<Message: 'static>(
2658 renderer: &mut Renderer,
2659 theme: &crate::Theme,
2660 style: &renderer::Style,
2661 cursor: mouse::Cursor,
2662 viewport: &Rectangle,
2663 color: Color,
2664 bounds: Rectangle,
2665 icon: Icon,
2666) {
2667 let layout_node = layout::Node::new(Size {
2668 width: bounds.width,
2669 height: bounds.width,
2670 })
2671 .move_to(Point {
2672 x: bounds.x,
2673 y: bounds.y,
2674 });
2675
2676 Widget::<Message, crate::Theme, Renderer>::draw(
2677 Element::<Message>::from(icon).as_widget(),
2678 &Tree::empty(),
2679 renderer,
2680 theme,
2681 &renderer::Style {
2682 icon_color: color,
2683 text_color: color,
2684 scale_factor: style.scale_factor,
2685 },
2686 Layout::new(&layout_node),
2687 cursor,
2688 viewport,
2689 );
2690}
2691
2692fn draw_drop_indicator(
2693 renderer: &mut Renderer,
2694 bounds: Rectangle,
2695 side: DropSide,
2696 vertical: bool,
2697 color: Color,
2698) {
2699 let thickness = 4.0;
2700 let quad_bounds = if vertical {
2701 let y = match side {
2702 DropSide::Before => bounds.y - thickness / 2.0,
2703 DropSide::After => bounds.y + bounds.height - thickness / 2.0,
2704 };
2705
2706 Rectangle {
2707 x: bounds.x,
2708 y,
2709 width: bounds.width,
2710 height: thickness,
2711 }
2712 } else {
2713 let x = match side {
2714 DropSide::Before => bounds.x - thickness / 2.0,
2715 DropSide::After => bounds.x + bounds.width - thickness / 2.0,
2716 };
2717
2718 Rectangle {
2719 x,
2720 y: bounds.y,
2721 width: thickness,
2722 height: bounds.height,
2723 }
2724 };
2725
2726 renderer.fill_quad(
2727 renderer::Quad {
2728 bounds: quad_bounds,
2729 border: Border {
2730 radius: 2.0.into(),
2731 ..Default::default()
2732 },
2733 shadow: Shadow::default(),
2734 snap: true,
2735 },
2736 Background::Color(color),
2737 );
2738}
2739
2740fn left_button_released(event: &Event) -> bool {
2741 matches!(
2742 event,
2743 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
2744 )
2745}
2746
2747fn right_button_released(event: &Event) -> bool {
2748 matches!(
2749 event,
2750 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,))
2751 )
2752}
2753
2754fn is_pressed(event: &Event) -> bool {
2755 matches!(
2756 event,
2757 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
2758 | Event::Touch(touch::Event::FingerPressed { .. })
2759 )
2760}
2761
2762fn is_lifted(event: &Event) -> bool {
2763 matches!(
2764 event,
2765 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
2766 | Event::Touch(touch::Event::FingerLifted { .. })
2767 )
2768}
2769
2770fn touch_lifted(event: &Event) -> bool {
2771 matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
2772}