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