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