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>) -> 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>) -> 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 }
942 window::Id::unique()
944 });
945 let Some(entity) = state.show_context else {
946 return;
947 };
948
949 let Some((mut bounds, i)) = self
950 .variant_bounds(state, layout.bounds())
951 .filter_map(|item| match item {
952 ItemBounds::Button(entity, bounds) => Some((bounds, entity)),
953 _ => None,
954 })
955 .enumerate()
956 .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })
957 else {
958 return;
959 };
960
961 assert!(
962 self.context_menu
963 .as_ref()
964 .is_none_or(|m| m[0].children.len() == self.model.len()),
965 "model length must match the number of context menus"
966 );
967 let menu = self
968 .context_menu
969 .as_mut()
970 .map(|m| m[0].children[i].clone())
971 .unwrap();
972
973 bounds.x = state.context_cursor.x;
974 bounds.y = state.context_cursor.y;
975
976 let mut popup_menu: menu::Menu<'static, _> = menu::Menu {
977 tree: my_state.clone(),
978 menu_roots: std::borrow::Cow::Owned(vec![menu]),
979 bounds_expand: 0,
980 menu_overlays_parent: false,
981 close_condition: CloseCondition {
982 leave: false,
983 click_outside: true,
984 click_inside: true,
985 },
986 item_width: ItemWidth::Uniform(240),
987 item_height: ItemHeight::Dynamic(40),
988 bar_bounds: bounds,
989 main_offset: 0,
990 cross_offset: 0,
991 root_bounds_list: vec![bounds],
992 path_highlight: Some(PathHighlight::MenuActive),
993 style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
994 position: Point::new(0., 0.),
995 is_overlay: false,
996 window_id: id,
997 depth: 0,
998 on_surface_action: self.on_surface_action.clone(),
999 };
1000
1001 menu::init_root_menu(
1002 &mut popup_menu,
1003 renderer,
1004 shell,
1005 view_cursor.position().unwrap(),
1006 viewport.size(),
1007 Vector::new(0., 0.),
1008 bounds,
1009 0., );
1011 let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| {
1012 state.popup_id.insert(self.window_id, id);
1013 (state
1014 .menu_states
1015 .iter()
1016 .find(|s| s.index.is_none())
1017 .map(|s| s.menu_bounds.parent_bounds)
1018 .map_or_else(
1019 || {
1020 let bounds = layout.bounds();
1021 Rectangle {
1022 x: bounds.x as i32,
1023 y: bounds.y as i32,
1024 width: 1,
1025 height: 1,
1026 }
1027 },
1028 |r| Rectangle {
1029 x: r.x as i32,
1030 y: r.y as i32,
1031 width: 1,
1032 height: 1,
1033 },
1034 ), match (state.horizontal_direction, state.vertical_direction) {
1035 (menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
1036 (menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
1037 (menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
1038 (menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
1039 })
1040 });
1041
1042 let menu_node =
1043 popup_menu.layout(renderer, layout::Limits::NONE.min_width(1.).min_height(1.));
1044 let popup_size = menu_node.size();
1045 let positioner = SctkPositioner {
1046 size: Some((
1047 popup_size.width.ceil() as u32 + 2,
1048 popup_size.height.ceil() as u32 + 2,
1049 )),
1050 anchor_rect,
1051 anchor:
1052 cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
1053 gravity,
1054 reactive: true,
1055 ..Default::default()
1056 };
1057 let parent = self.window_id;
1058
1059 let t = THEME.lock().unwrap();
1060 let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
1061 drop(t);
1062 let rad = styling.menu_border_radius;
1063
1064 #[cfg(wayland_platform)]
1066 #[must_use]
1067 pub fn simple_popup<Message: 'static>(
1068 live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static,
1069 settings: impl Fn()
1070 -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
1071 + Send
1072 + Sync
1073 + 'static,
1074 view: Option<impl Fn() -> crate::Element<'static, Message> + Send + Sync + 'static>,
1075 ) -> crate::surface::Action<Message> {
1076 use std::any::Any;
1077
1078 let boxed: Box<
1079 dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
1080 + Send
1081 + Sync
1082 + 'static,
1083 > = Box::new(settings);
1084 let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
1085
1086 let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
1087 Box::new(live_settings);
1088 let boxed_live: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed_live);
1089
1090 crate::surface::Action::Popup(
1091 Arc::new(boxed),
1092 Arc::new(boxed_live),
1093 view.map(|view| {
1094 Arc::new(move || view().map(crate::Action::App))
1095 as crate::surface::View<Message>
1096 }),
1097 )
1098 }
1099 shell.publish((surface_action)(simple_popup(
1100 move || LiveSettings {
1101 corners: Some(CornerRadius {
1102 top_left: rad[0] as u32,
1103 top_right: rad[1] as u32,
1104 bottom_left: rad[2] as u32,
1105 bottom_right: rad[3] as u32,
1106 }),
1107 ..Default::default()
1108 },
1109 move || SctkPopupSettings {
1110 parent,
1111 id,
1112 positioner: positioner.clone(),
1113 parent_size: None,
1114 grab: true,
1115 close_with_children: false,
1116 input_zone: None,
1117 },
1118 Some(move || {
1119 Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill))
1120 }),
1121 )));
1122 }
1123 }
1124}
1125
1126impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
1127 for SegmentedButton<'_, Variant, SelectionMode, Message>
1128where
1129 Self: SegmentedVariant,
1130 Model<SelectionMode>: Selectable,
1131 SelectionMode: Default,
1132 Message: 'static + Clone,
1133{
1134 fn id(&self) -> Option<widget::Id> {
1135 Some(self.id.0.clone())
1136 }
1137
1138 fn set_id(&mut self, id: widget::Id) {
1139 self.id = Id(id);
1140 }
1141
1142 fn children(&self) -> Vec<Tree> {
1143 let mut children = Vec::new();
1144
1145 if let Some(ref context_menu) = self.context_menu {
1147 let mut tree = Tree::empty();
1148 tree.state = tree::State::new(MenuBarState::default());
1149 tree.children = menu_roots_children(context_menu);
1150 children.push(tree);
1151 }
1152
1153 children
1154 }
1155
1156 fn tag(&self) -> tree::Tag {
1157 tree::Tag::of::<LocalState>()
1158 }
1159
1160 fn state(&self) -> tree::State {
1161 #[allow(clippy::default_trait_access)]
1162 tree::State::new(LocalState {
1163 menu_state: Default::default(),
1164 paragraphs: SecondaryMap::new(),
1165 text_hashes: SecondaryMap::new(),
1166 buttons_visible: Default::default(),
1167 buttons_offset: Default::default(),
1168 collapsed: Default::default(),
1169 focused: Default::default(),
1170 focused_item: Default::default(),
1171 focused_visible: false,
1172 hovered: Default::default(),
1173 known_length: Default::default(),
1174 middle_clicked: Default::default(),
1175 internal_layout: Default::default(),
1176 context_cursor: Point::default(),
1177 show_context: Default::default(),
1178 wheel_timestamp: Default::default(),
1179 dnd_state: Default::default(),
1180 fingers_pressed: Default::default(),
1181 pressed_item: None,
1182 tab_drag_candidate: None,
1183 dragging_tab: None,
1184 drop_hint: None,
1185 offer_mimes: Vec::new(),
1186 })
1187 }
1188
1189 fn diff(&mut self, tree: &mut Tree) {
1190 let state = tree.state.downcast_mut::<LocalState>();
1191 for key in self.model.order.iter().copied() {
1192 self.update_entity_paragraph(state, key);
1193 }
1194
1195 if let Some(context_menu) = &mut self.context_menu {
1197 state.menu_state.inner.with_data_mut(|inner| {
1198 menu_roots_diff(context_menu, &mut inner.tree);
1199 });
1200 }
1201
1202 if let Some(f) = state.focused.as_ref()
1204 && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get())
1205 {
1206 state.unfocus();
1207 }
1208 }
1209
1210 fn size(&self) -> Size<Length> {
1211 Size::new(self.width, self.height)
1212 }
1213
1214 fn layout(
1215 &mut self,
1216 tree: &mut Tree,
1217 renderer: &Renderer,
1218 limits: &layout::Limits,
1219 ) -> layout::Node {
1220 let state = tree.state.downcast_mut::<LocalState>();
1221 let limits = limits.shrink(self.padding);
1222 let size = self
1223 .variant_layout(state, renderer, &limits)
1224 .expand(self.padding);
1225 layout::Node::new(size)
1226 }
1227
1228 #[allow(clippy::too_many_lines)]
1229 fn update(
1230 &mut self,
1231 tree: &mut Tree,
1232 mut event: &Event,
1233 layout: Layout<'_>,
1234 cursor_position: mouse::Cursor,
1235 renderer: &Renderer,
1236 clipboard: &mut dyn Clipboard,
1237 shell: &mut Shell<'_, Message>,
1238 viewport: &iced::Rectangle,
1239 ) {
1240 let my_bounds = layout.bounds();
1241 let state = tree.state.downcast_mut::<LocalState>();
1242
1243 #[cfg(wayland_platform)]
1245 if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
1246 iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup),
1247 )) = &event
1248 {
1249 let dismissed = state.menu_state.inner.with_data_mut(|data| {
1250 if data.popup_id.get(&self.window_id) == Some(popup) {
1251 data.popup_id.clear();
1252 data.reset();
1253 true
1254 } else {
1255 false
1256 }
1257 });
1258 if dismissed {
1259 state.show_context = None;
1260 for key in self.model.order.iter().copied() {
1261 self.update_entity_paragraph(state, key);
1262 }
1263 }
1264 }
1265
1266 let hovered_before = state.hovered;
1267
1268 let my_id = self.get_drag_id();
1269
1270 if let Event::Dnd(e) = &mut event {
1271 let entity = state
1272 .dnd_state
1273 .drag_offer
1274 .as_ref()
1275 .map(|dnd_state| dnd_state.data);
1276 log::trace!(
1277 target: TAB_REORDER_LOG_TARGET,
1278 "segmented button {:?} received DnD event: {:?} entity={entity:?}",
1279 my_id,
1280 e
1281 );
1282 match e {
1283 DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => {
1284 if state.dragging_tab.take().is_some() {
1285 state.tab_drag_candidate = None;
1286 state.drop_hint = None;
1287 self.emit_drop_hint(shell, state.drop_hint);
1288 log::trace!(
1289 target: TAB_REORDER_LOG_TARGET,
1290 "tab drag source finished id={:?}",
1291 my_id
1292 );
1293 shell.capture_event();
1294 return;
1295 }
1296 }
1297 DndEvent::Offer(
1298 id,
1299 OfferEvent::Enter {
1300 x, y, mime_types, ..
1301 },
1302 ) if Some(my_id) == *id => {
1303 let entity = self
1304 .variant_bounds(state, my_bounds)
1305 .filter_map(|item| match item {
1306 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1307 _ => None,
1308 })
1309 .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1310 .map(|(key, _)| key);
1311 state.drop_hint = self.drop_hint_for_position(
1312 state,
1313 my_bounds,
1314 Point::new(*x as f32, *y as f32),
1315 );
1316 self.emit_drop_hint(shell, state.drop_hint);
1317 log::trace!(
1318 target: TAB_REORDER_LOG_TARGET,
1319 "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}"
1320 );
1321 if let Some(entity) = entity {
1323 state.hovered = Item::Tab(entity);
1324 for key in self.model.order.iter().copied() {
1325 self.update_entity_paragraph(state, key);
1326 }
1327 }
1328
1329 let on_dnd_enter = self
1330 .on_dnd_enter
1331 .as_ref()
1332 .zip(entity)
1333 .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes));
1334 let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime)
1335 && mime_types.is_empty()
1336 {
1337 vec![mime.clone()]
1338 } else {
1339 mime_types.clone()
1340 };
1341 state.offer_mimes.clone_from(&mimes);
1342
1343 _ = state
1344 .dnd_state
1345 .on_enter::<Message>(*x, *y, mimes, on_dnd_enter, entity);
1346 }
1347 DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {}
1348 DndEvent::Offer(id, leave)
1349 if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination)
1350 && Some(my_id) == *id =>
1351 {
1352 state.drop_hint = None;
1353 self.emit_drop_hint(shell, state.drop_hint);
1354 if let Some(Some(entity)) = entity {
1355 if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1356 shell.publish(on_dnd_leave(entity));
1357 }
1358 }
1359 log::trace!(
1360 target: TAB_REORDER_LOG_TARGET,
1361 "offer leave id={my_id:?} entity={entity:?}"
1362 );
1363 state.hovered = Item::None;
1364 for key in self.model.order.iter().copied() {
1365 self.update_entity_paragraph(state, key);
1366 }
1367 _ = state.dnd_state.on_leave::<Message>(None);
1368 }
1369 DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => {
1370 log::trace!(
1371 target: TAB_REORDER_LOG_TARGET,
1372 "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}"
1373 );
1374 let new = self
1375 .variant_bounds(state, my_bounds)
1376 .filter_map(|item| match item {
1377 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1378 _ => None,
1379 })
1380 .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32)))
1381 .map(|(key, _)| key);
1382 if let Some(new_entity) = new {
1383 state.dnd_state.on_motion::<Message>(
1384 *x,
1385 *y,
1386 None::<fn(_, _) -> Message>,
1387 None::<fn(_, _, _) -> Message>,
1388 Some(new_entity),
1389 );
1390 state.drop_hint = self.drop_hint_for_position(
1391 state,
1392 my_bounds,
1393 Point::new(*x as f32, *y as f32),
1394 );
1395 self.emit_drop_hint(shell, state.drop_hint);
1396 if Some(Some(new_entity)) != entity {
1397 state.hovered = Item::Tab(new_entity);
1398 for key in self.model.order.iter().copied() {
1399 self.update_entity_paragraph(state, key);
1400 }
1401 let prev_action = state
1402 .dnd_state
1403 .drag_offer
1404 .as_ref()
1405 .map(|dnd| dnd.selected_action);
1406 if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() {
1407 shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone()));
1408 }
1409 if let Some(dnd) = state.dnd_state.drag_offer.as_mut() {
1410 dnd.data = Some(new_entity);
1411 if let Some(prev_action) = prev_action {
1412 dnd.selected_action = prev_action;
1413 }
1414 }
1415 }
1416 } else if entity.is_some() {
1417 state.hovered = Item::None;
1418 for key in self.model.order.iter().copied() {
1419 self.update_entity_paragraph(state, key);
1420 }
1421 log::trace!(
1422 target: TAB_REORDER_LOG_TARGET,
1423 "offer motion leaving id={my_id:?}"
1424 );
1425 state.drop_hint = None;
1426 self.emit_drop_hint(shell, state.drop_hint);
1427 state.dnd_state.on_motion::<Message>(
1428 *x,
1429 *y,
1430 None::<fn(_, _) -> Message>,
1431 None::<fn(_, _, _) -> Message>,
1432 None,
1433 );
1434 if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() {
1435 if let Some(Some(entity)) = entity {
1436 shell.publish(on_dnd_leave(entity));
1437 }
1438 }
1439 }
1440 }
1441 DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => {
1442 log::trace!(
1443 target: TAB_REORDER_LOG_TARGET,
1444 "offer drop id={my_id:?} entity={entity:?}"
1445 );
1446 _ = state
1447 .dnd_state
1448 .on_drop::<Message>(None::<fn(_, _) -> Message>);
1449 }
1450 DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => {
1451 if state.dnd_state.drag_offer.is_some() {
1452 log::trace!(
1453 target: TAB_REORDER_LOG_TARGET,
1454 "offer selected action id={my_id:?} action={action:?} entity={entity:?}"
1455 );
1456 _ = state
1457 .dnd_state
1458 .on_action_selected::<Message>(*action, None::<fn(_) -> Message>);
1459 }
1460 }
1461 DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => {
1462 log::trace!(
1463 target: TAB_REORDER_LOG_TARGET,
1464 "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}"
1465 );
1466 let drop_entity = entity
1467 .flatten()
1468 .or_else(|| state.drop_hint.map(|hint| hint.entity));
1469 let allow_reorder = state
1470 .dnd_state
1471 .drag_offer
1472 .as_ref()
1473 .is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
1474 let pending_reorder = if allow_reorder
1475 && self.on_reorder.is_some()
1476 && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
1477 && state.dragging_tab.is_some()
1478 {
1479 drop_entity.and_then(|target| self.reorder_event_for_drop(state, target))
1480 } else {
1481 None
1482 };
1483 if let Some(entity) = drop_entity {
1484 let on_drop = self.on_dnd_drop.as_ref();
1485 let on_drop = on_drop.map(|on_drop| {
1486 |mime, data, action, _, _| on_drop(entity, data, mime, action)
1487 });
1488
1489 let (maybe_msg, ret) = state.dnd_state.on_data_received(
1490 mime_type.clone(),
1491 data.clone(),
1492 None::<fn(_, _) -> Message>,
1493 on_drop,
1494 );
1495 if matches!(ret, iced::event::Status::Captured) {
1496 shell.capture_event();
1497 }
1498 if let Some(msg) = maybe_msg {
1499 log::trace!(
1500 target: TAB_REORDER_LOG_TARGET,
1501 "publishing drop message entity={entity:?}"
1502 );
1503 shell.publish(msg);
1504 }
1505 state.drop_hint = None;
1506
1507 self.emit_drop_hint(shell, state.drop_hint);
1508 if let Some(event) = pending_reorder {
1509 state.focused_item = Item::Tab(event.dragged);
1510 state.hovered = Item::None;
1511 for key in self.model.order.iter().copied() {
1512 self.update_entity_paragraph(state, key);
1513 }
1514 if let Some(on_reorder) = self.on_reorder.as_ref() {
1515 shell.publish(on_reorder(event));
1516 shell.capture_event();
1517 return;
1518 }
1519 }
1520 return;
1521 }
1522 }
1523 _ => {}
1524 }
1525 }
1526
1527 if cursor_position.is_over(my_bounds) {
1528 let fingers_pressed = state.fingers_pressed.len();
1529
1530 match event {
1531 Event::Touch(touch::Event::FingerPressed { id, .. }) => {
1532 state.fingers_pressed.insert(*id);
1533 }
1534
1535 Event::Touch(touch::Event::FingerLifted { id, .. }) => {
1536 state.fingers_pressed.remove(id);
1537 }
1538 _ => (),
1539 }
1540
1541 if state.collapsed {
1543 if cursor_position
1545 .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height)))
1546 && self.prev_tab_sensitive(state)
1547 {
1548 state.hovered = Item::PrevButton;
1549 for key in self.model.order.iter().copied() {
1550 self.update_entity_paragraph(state, key);
1551 }
1552 if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1553 | Event::Touch(touch::Event::FingerLifted { .. }) = event
1554 {
1555 state.buttons_offset -= 1;
1556 }
1557 } else {
1558 if cursor_position
1560 .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height)))
1561 && self.next_tab_sensitive(state)
1562 {
1563 state.hovered = Item::NextButton;
1564 for key in self.model.order.iter().copied() {
1565 self.update_entity_paragraph(state, key);
1566 }
1567 if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1568 | Event::Touch(touch::Event::FingerLifted { .. }) = event
1569 {
1570 state.buttons_offset += 1;
1571 }
1572 }
1573 }
1574 }
1575
1576 for (key, bounds) in self
1577 .variant_bounds(state, my_bounds)
1578 .filter_map(|item| match item {
1579 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1580 _ => None,
1581 })
1582 .collect::<Vec<_>>()
1583 {
1584 if cursor_position.is_over(bounds) {
1585 if self.model.items[key].enabled {
1586 if state.hovered != Item::Tab(key) {
1588 state.hovered = Item::Tab(key);
1589 for key in self.model.order.iter().copied() {
1590 self.update_entity_paragraph(state, key);
1591 }
1592 }
1593
1594 let close_button_bounds =
1595 close_bounds(bounds, f32::from(self.close_icon.size));
1596 let over_close_button = self.model.items[key].closable
1597 && cursor_position.is_over(close_button_bounds);
1598
1599 if self.model.items[key].closable {
1601 if let Some(on_close) = self.on_close.as_ref() {
1603 if over_close_button
1604 && (left_button_released(&event)
1605 || (touch_lifted(&event) && fingers_pressed == 1))
1606 {
1607 shell.publish(on_close(key));
1608 shell.capture_event();
1609 return;
1610 }
1611
1612 if self.on_middle_press.is_none() {
1613 if let Event::Mouse(mouse::Event::ButtonReleased(
1615 mouse::Button::Middle,
1616 )) = event
1617 {
1618 if state.middle_clicked == Some(Item::Tab(key)) {
1619 shell.publish(on_close(key));
1620 shell.capture_event();
1621 return;
1622 }
1623
1624 state.middle_clicked = None;
1625 }
1626 }
1627 }
1628 }
1629
1630 if self.tab_drag.is_some()
1631 && matches!(
1632 event,
1633 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
1634 )
1635 && !over_close_button
1636 && let Some(position) = cursor_position.position()
1637 {
1638 state.tab_drag_candidate = Some(TabDragCandidate {
1639 entity: key,
1640 bounds,
1641 origin: position,
1642 });
1643 if let Some(tab_drag) = self.tab_drag.as_ref() {
1644 log::trace!(
1645 target: TAB_REORDER_LOG_TARGET,
1646 "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}",
1647 key,
1648 position.x,
1649 position.y,
1650 bounds.x,
1651 bounds.y,
1652 bounds.width,
1653 bounds.height,
1654 tab_drag.threshold
1655 );
1656 }
1657 }
1658
1659 if is_lifted(&event) {
1660 state.unfocus();
1661 }
1662
1663 #[cfg(wayland_platform)]
1664 if is_pressed(event)
1665 && let Some(on_context) = self.on_context.as_ref()
1666 {
1667 let (was_open, id) = state.menu_state.inner.with_data_mut(|data| {
1668 let was_open = data.open;
1669 data.reset();
1670 data.open = false;
1671 data.view_cursor = cursor_position;
1672 let root = data.popup_id.remove(&self.window_id);
1673 data.popup_id.clear();
1674 (was_open, root)
1675 });
1676 if let Some(w) = id
1677 && let Some(surface_action) = self.on_surface_action.as_ref()
1678 && was_open
1679 {
1680 use crate::surface::action::destroy_popup;
1681
1682 shell.publish((surface_action)(destroy_popup(w)));
1683 return;
1684 }
1685 }
1686
1687 if let Some(on_activate) = self.on_activate.as_ref() {
1688 if is_pressed(event) {
1689 state.pressed_item = Some(Item::Tab(key));
1690 } else if is_lifted(&event) && self.button_is_pressed(state, key) {
1691 shell.publish(on_activate(key));
1692 state.set_focused();
1693 state.focused_item = Item::Tab(key);
1694 state.pressed_item = None;
1695 shell.capture_event();
1696 return;
1697 }
1698 }
1699
1700 if self.context_menu.is_some()
1702 && let Some(on_context) = self.on_context.as_ref()
1703 && (right_button_released(&event)
1704 || (touch_lifted(&event) && fingers_pressed == 2))
1705 {
1706 state.show_context = Some(key);
1707 state.context_cursor = cursor_position.position().unwrap_or_default();
1708
1709 state.menu_state.inner.with_data_mut(|data| {
1710 data.reset();
1712 data.open = true;
1713 data.view_cursor = cursor_position;
1714 });
1715
1716 shell.publish(on_context(key));
1717 shell.capture_event();
1718
1719 #[cfg(wayland_platform)]
1720 if matches!(
1721 crate::app::cosmic::WINDOWING_SYSTEM.get(),
1722 Some(crate::app::cosmic::WindowingSystem::Wayland)
1723 ) {
1724 self.create_popup(
1725 layout,
1726 cursor_position,
1727 renderer,
1728 shell,
1729 viewport,
1730 tree,
1731 );
1732 }
1733 return;
1734 }
1735 if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) =
1736 event
1737 {
1738 state.middle_clicked = Some(Item::Tab(key));
1739 if let Some(on_middle_press) = self.on_middle_press.as_ref() {
1740 shell.publish(on_middle_press(key));
1741 shell.capture_event();
1742 return;
1743 }
1744 }
1745 }
1746
1747 break;
1748 } else if state.hovered == Item::Tab(key) {
1749 state.hovered = Item::None;
1750 self.update_entity_paragraph(state, key);
1751 }
1752 }
1753
1754 if self.scrollable_focus
1755 && let Some(on_activate) = self.on_activate.as_ref()
1756 && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
1757 {
1758 let current = Instant::now();
1759
1760 if state.wheel_timestamp.is_none_or(|previous| {
1762 current.duration_since(previous) > Duration::from_millis(250)
1763 }) {
1764 state.wheel_timestamp = Some(current);
1765
1766 match delta {
1767 ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => {
1768 let mut activate_key = None;
1769
1770 if *y < 0.0 {
1771 let mut prev_key = Entity::null();
1772
1773 for key in self.model.order.iter().copied() {
1774 if self.model.is_active(key) && !prev_key.is_null() {
1775 activate_key = Some(prev_key);
1776 }
1777
1778 if self.model.is_enabled(key) {
1779 prev_key = key;
1780 }
1781 }
1782 } else if *y > 0.0 {
1783 let mut buttons = self.model.order.iter().copied();
1784 while let Some(key) = buttons.next() {
1785 if self.model.is_active(key) {
1786 for key in buttons {
1787 if self.model.is_enabled(key) {
1788 activate_key = Some(key);
1789 break;
1790 }
1791 }
1792 break;
1793 }
1794 }
1795 }
1796
1797 if let Some(key) = activate_key {
1798 shell.publish(on_activate(key));
1799 state.set_focused();
1800 state.focused_item = Item::Tab(key);
1801 shell.capture_event();
1802 return;
1803 }
1804 }
1805 }
1806 }
1807 }
1808 } else {
1809 if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) {
1810 for key in self.model.order.iter().copied() {
1811 self.update_entity_paragraph(state, key);
1812 }
1813 }
1814 if state.is_focused() {
1815 if is_pressed(&event) {
1817 state.unfocus();
1818 state.pressed_item = None;
1819 return;
1820 }
1821 } else if is_lifted(&event) {
1822 state.pressed_item = None;
1823 }
1824 }
1825
1826 if let (Some(tab_drag), Some(candidate)) =
1827 (self.tab_drag.as_ref(), state.tab_drag_candidate)
1828 && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
1829 && let Some(position) = cursor_position.position()
1830 && position.distance(candidate.origin) >= tab_drag.threshold
1831 && let Some(candidate) = state.tab_drag_candidate.take()
1832 {
1833 log::trace!(
1834 target: TAB_REORDER_LOG_TARGET,
1835 "tab drag threshold met entity={:?} distance={:.2} threshold={}",
1836 candidate.entity,
1837 position.distance(candidate.origin),
1838 tab_drag.threshold
1839 );
1840 if self.start_tab_drag(
1841 state,
1842 candidate.entity,
1843 candidate.bounds,
1844 position,
1845 clipboard,
1846 ) {
1847 shell.capture_event();
1848 return;
1849 }
1850 }
1851
1852 if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event)))
1853 && let Some(_id) = state
1854 .menu_state
1855 .inner
1856 .with_data_mut(|ms| ms.popup_id.remove(&self.window_id))
1857 {
1858 #[cfg(wayland_platform)]
1859 {
1860 let surface_action = self.on_surface_action.as_ref().unwrap();
1861 shell.capture_event();
1862
1863 shell.publish(surface_action(crate::surface::action::destroy_popup(_id)));
1864 }
1865 state.show_context = None;
1866
1867 state.menu_state.inner.with_data_mut(|data| {
1868 data.reset();
1870 data.open = false;
1871 data.view_cursor = cursor_position;
1872 });
1873 }
1874
1875 if matches!(
1876 event,
1877 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
1878 ) {
1879 state.tab_drag_candidate = None;
1880 }
1881
1882 if state.is_focused() {
1883 if let Event::Keyboard(keyboard::Event::KeyPressed {
1884 key: keyboard::Key::Named(keyboard::key::Named::Tab),
1885 modifiers,
1886 ..
1887 }) = event
1888 {
1889 shell.request_redraw();
1890 state.focused_visible = true;
1891 return if *modifiers == keyboard::Modifiers::SHIFT {
1892 self.focus_previous(state, shell);
1893 } else if modifiers.is_empty() {
1894 self.focus_next(state, shell);
1895 };
1896 }
1897
1898 if let Some(on_activate) = self.on_activate.as_ref()
1899 && let Event::Keyboard(keyboard::Event::KeyReleased {
1900 key: keyboard::Key::Named(keyboard::key::Named::Enter),
1901 ..
1902 }) = event
1903 {
1904 match state.focused_item {
1905 Item::Tab(entity) => {
1906 shell.publish(on_activate(entity));
1907 }
1908
1909 Item::PrevButton => {
1910 if self.prev_tab_sensitive(state) {
1911 state.buttons_offset -= 1;
1912
1913 if !self.prev_tab_sensitive(state)
1915 && let Some(first) = self.first_tab(state)
1916 {
1917 state.focused_item = Item::Tab(first);
1918 }
1919 }
1920 }
1921
1922 Item::NextButton => {
1923 if self.next_tab_sensitive(state) {
1924 state.buttons_offset += 1;
1925
1926 if !self.next_tab_sensitive(state)
1928 && let Some(last) = self.last_tab(state)
1929 {
1930 state.focused_item = Item::Tab(last);
1931 }
1932 }
1933 }
1934
1935 Item::None | Item::Set => (),
1936 }
1937
1938 shell.capture_event();
1939 }
1940 }
1941
1942 if hovered_before != state.hovered {
1943 shell.request_redraw();
1944 }
1945 }
1946
1947 fn operate(
1948 &mut self,
1949 tree: &mut Tree,
1950 layout: Layout<'_>,
1951 _renderer: &Renderer,
1952 operation: &mut dyn iced_core::widget::Operation<()>,
1953 ) {
1954 let state = tree.state.downcast_mut::<LocalState>();
1955 operation.focusable(Some(&self.id.0), layout.bounds(), state);
1956 operation.custom(Some(&self.id.0), layout.bounds(), state);
1957
1958 if let Item::Set = state.focused_item {
1959 if self.prev_tab_sensitive(state) {
1960 state.focused_item = Item::PrevButton;
1961 } else if let Some(first) = self.first_tab(state) {
1962 state.focused_item = Item::Tab(first);
1963 }
1964 }
1965 }
1966
1967 fn mouse_interaction(
1968 &self,
1969 tree: &Tree,
1970 layout: Layout<'_>,
1971 cursor_position: mouse::Cursor,
1972 _viewport: &iced::Rectangle,
1973 _renderer: &Renderer,
1974 ) -> iced_core::mouse::Interaction {
1975 if self.on_activate.is_none() {
1976 return iced_core::mouse::Interaction::default();
1977 }
1978 let state = tree.state.downcast_ref::<LocalState>();
1979 let bounds = layout.bounds();
1980
1981 if cursor_position.is_over(bounds) {
1982 let hovered_button = self
1983 .variant_bounds(state, bounds)
1984 .filter_map(|item| match item {
1985 ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
1986 _ => None,
1987 })
1988 .find(|(_key, bounds)| cursor_position.is_over(*bounds));
1989
1990 if let Some((key, _bounds)) = hovered_button {
1991 return if self.model.items[key].enabled {
1992 iced_core::mouse::Interaction::Pointer
1993 } else {
1994 iced_core::mouse::Interaction::Idle
1995 };
1996 }
1997 }
1998
1999 iced_core::mouse::Interaction::default()
2000 }
2001
2002 #[allow(clippy::too_many_lines)]
2003 fn draw(
2004 &self,
2005 tree: &Tree,
2006 renderer: &mut Renderer,
2007 theme: &crate::Theme,
2008 style: &renderer::Style,
2009 layout: Layout<'_>,
2010 cursor: mouse::Cursor,
2011 viewport: &iced::Rectangle,
2012 ) {
2013 let state = tree.state.downcast_ref::<LocalState>();
2014 let appearance = Self::variant_appearance(theme, &self.style);
2015 let bounds: Rectangle = layout.bounds();
2016 let button_amount = self.model.items.len();
2017 let show_drop_hint = state.dragging_tab.is_some();
2018 let drop_hint = if show_drop_hint {
2019 state.drop_hint
2020 } else {
2021 None
2022 };
2023
2024 if let Some(background) = appearance.background {
2026 renderer.fill_quad(
2027 renderer::Quad {
2028 bounds,
2029 border: appearance.border,
2030 shadow: Shadow::default(),
2031 snap: true,
2032 },
2033 background,
2034 );
2035 }
2036
2037 if state.collapsed {
2039 let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height));
2040
2041 let mut background_appearance =
2043 if self.on_activate.is_some() && Item::PrevButton == state.focused_item {
2044 Some(appearance.active)
2045 } else if self.on_activate.is_some() && Item::PrevButton == state.hovered {
2046 Some(appearance.hover)
2047 } else {
2048 None
2049 };
2050
2051 if let Some(background_appearance) = background_appearance.take() {
2052 renderer.fill_quad(
2053 renderer::Quad {
2054 bounds: tab_bounds,
2055 border: Border {
2056 radius: theme.cosmic().radius_s().into(),
2057 ..Default::default()
2058 },
2059 shadow: Shadow::default(),
2060 snap: true,
2061 },
2062 background_appearance
2063 .background
2064 .unwrap_or(Background::Color(Color::TRANSPARENT)),
2065 );
2066 }
2067
2068 draw_icon::<Message>(
2069 renderer,
2070 theme,
2071 style,
2072 cursor,
2073 viewport,
2074 if state.buttons_offset == 0 {
2075 appearance.inactive.text_color
2076 } else {
2077 appearance.active.text_color
2078 },
2079 Rectangle {
2080 x: tab_bounds.x + 8.0,
2081 y: tab_bounds.y + f32::from(self.button_height) / 4.0,
2082 width: 16.0,
2083 height: 16.0,
2084 },
2085 icon::from_name("go-previous-symbolic").size(16).icon(),
2086 );
2087
2088 tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height));
2089
2090 background_appearance =
2092 if self.on_activate.is_some() && Item::NextButton == state.focused_item {
2093 Some(appearance.active)
2094 } else if self.on_activate.is_some() && Item::NextButton == state.hovered {
2095 Some(appearance.hover)
2096 } else {
2097 None
2098 };
2099
2100 if let Some(background_appearance) = background_appearance {
2101 renderer.fill_quad(
2102 renderer::Quad {
2103 bounds: tab_bounds,
2104 border: Border {
2105 radius: theme.cosmic().radius_s().into(),
2106 ..Default::default()
2107 },
2108 shadow: Shadow::default(),
2109 snap: true,
2110 },
2111 background_appearance
2112 .background
2113 .unwrap_or(Background::Color(Color::TRANSPARENT)),
2114 );
2115 }
2116
2117 draw_icon::<Message>(
2118 renderer,
2119 theme,
2120 style,
2121 cursor,
2122 viewport,
2123 if self.next_tab_sensitive(state) {
2124 appearance.active.text_color
2125 } else if let Item::NextButton = state.focused_item {
2126 appearance.active.text_color
2127 } else {
2128 appearance.inactive.text_color
2129 },
2130 Rectangle {
2131 x: tab_bounds.x + 8.0,
2132 y: tab_bounds.y + f32::from(self.button_height) / 4.0,
2133 width: 16.0,
2134 height: 16.0,
2135 },
2136 icon::from_name("go-next-symbolic").size(16).icon(),
2137 );
2138 }
2139
2140 let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
2141
2142 let divider_background = Background::Color(
2143 crate::theme::active()
2144 .cosmic()
2145 .primary_component_divider()
2146 .into(),
2147 );
2148
2149 let mut nth = 0;
2151 let drop_hint_marker = drop_hint;
2152 let show_drop_hint_marker = show_drop_hint;
2153 self.variant_bounds(state, bounds).for_each(move |item| {
2154 let (key, mut bounds) = match item {
2155 ItemBounds::Button(entity, bounds) => (entity, bounds),
2157
2158 ItemBounds::Divider(bounds, accented) => {
2160 renderer.fill_quad(
2161 renderer::Quad {
2162 bounds,
2163 border: Border::default(),
2164 shadow: Shadow::default(),
2165 snap: true,
2166 },
2167 {
2168 let theme = crate::theme::active();
2169 if accented {
2170 Background::Color(theme.cosmic().small_widget_divider().into())
2171 } else {
2172 Background::Color(theme.cosmic().primary_container_divider().into())
2173 }
2174 },
2175 );
2176
2177 return;
2178 }
2179 };
2180
2181 let original_bounds = bounds;
2182 let center_y = bounds.center_y();
2183
2184 if show_drop_hint_marker
2185 && matches!(
2186 drop_hint_marker,
2187 Some(DropHint {
2188 entity,
2189 side: DropSide::Before
2190 }) if entity == key
2191 )
2192 {
2193 draw_drop_indicator(
2194 renderer,
2195 original_bounds,
2196 DropSide::Before,
2197 Self::VERTICAL,
2198 appearance.active.text_color,
2199 );
2200 }
2201
2202 let menu_open = || {
2203 state.show_context == Some(key)
2204 && state.menu_state.inner.with_data(|data| data.open)
2205 };
2206
2207 let key_is_active = self.model.is_active(key);
2208 let key_is_focused = state.focused_visible && self.button_is_focused(state, key);
2209 let key_is_hovered = self.button_is_hovered(state, key);
2210 let status_appearance = if self.button_is_pressed(state, key) {
2211 appearance.pressed
2212 } else if key_is_hovered || menu_open() {
2213 appearance.hover
2214 } else if key_is_active {
2215 appearance.active
2216 } else {
2217 appearance.inactive
2218 };
2219
2220 let button_appearance = if nth == 0 {
2221 status_appearance.first
2222 } else if nth + 1 == button_amount {
2223 status_appearance.last
2224 } else {
2225 status_appearance.middle
2226 };
2227
2228 if appearance.active_width > 0.0 {
2230 let active_width = if key_is_active {
2231 appearance.active_width
2232 } else {
2233 1.0
2234 };
2235
2236 renderer.fill_quad(
2237 renderer::Quad {
2238 bounds: if Self::VERTICAL {
2239 Rectangle {
2240 x: bounds.x + bounds.width - active_width,
2241 width: active_width,
2242 ..bounds
2243 }
2244 } else {
2245 Rectangle {
2246 y: bounds.y + bounds.height - active_width,
2247 height: active_width,
2248 ..bounds
2249 }
2250 },
2251 border: Border {
2252 radius: rad_0.into(),
2253 ..Default::default()
2254 },
2255 shadow: Shadow::default(),
2256 snap: true,
2257 },
2258 appearance.active.text_color,
2259 );
2260 }
2261
2262 bounds.x += f32::from(self.button_padding[0]);
2263 bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
2264 let mut indent_padding = 0.0;
2265
2266 if let Some(indent) = self.model.indent(key)
2268 && indent > 0
2269 {
2270 let adjustment = f32::from(indent) * f32::from(self.indent_spacing);
2271 bounds.x += adjustment;
2272 bounds.width -= adjustment;
2273
2274 if let crate::theme::SegmentedButton::FileNav = self.style
2276 && indent > 1
2277 {
2278 indent_padding = 7.0;
2279
2280 for level in 1..indent {
2281 renderer.fill_quad(
2282 renderer::Quad {
2283 bounds: Rectangle {
2284 x: (level as f32)
2285 .mul_add(-(self.indent_spacing as f32), bounds.x)
2286 + indent_padding,
2287 width: 1.0,
2288 ..bounds
2289 },
2290 border: Border {
2291 radius: rad_0.into(),
2292 ..Default::default()
2293 },
2294 shadow: Shadow::default(),
2295 snap: true,
2296 },
2297 divider_background,
2298 );
2299 }
2300
2301 indent_padding += 4.0;
2302 }
2303 }
2304
2305 if key_is_focused || status_appearance.background.is_some() {
2307 renderer.fill_quad(
2308 renderer::Quad {
2309 bounds: Rectangle {
2310 x: bounds.x - f32::from(self.button_padding[0]) + indent_padding,
2311 width: bounds.width + f32::from(self.button_padding[0])
2312 - f32::from(self.button_padding[2])
2313 - indent_padding,
2314 ..bounds
2315 },
2316 border: if key_is_focused {
2317 Border {
2318 width: 1.0,
2319 color: appearance.active.text_color,
2320 radius: button_appearance.border.radius,
2321 }
2322 } else {
2323 button_appearance.border
2324 },
2325 shadow: Shadow::default(),
2326 snap: true,
2327 },
2328 status_appearance
2329 .background
2330 .unwrap_or(Background::Color(Color::TRANSPARENT)),
2331 );
2332 }
2333
2334 {
2336 let actual_width = state.internal_layout[nth].1.width.min(bounds.width);
2339
2340 let offset = match self.button_alignment {
2341 Alignment::Start => None,
2342 Alignment::Center => Some((bounds.width - actual_width) / 2.0),
2343 Alignment::End => Some(bounds.width - actual_width),
2344 };
2345
2346 if let Some(offset) = offset {
2347 bounds.x += offset - f32::from(self.button_padding[0]);
2348 bounds.width = actual_width;
2349 }
2350 }
2351
2352 if let Some(icon) = self.model.icon(key) {
2354 let mut image_bounds = bounds;
2355 let width = f32::from(icon.size);
2356 let offset = width + f32::from(self.button_spacing);
2357 image_bounds.y = center_y - width / 2.0;
2358
2359 draw_icon::<Message>(
2360 renderer,
2361 theme,
2362 style,
2363 cursor,
2364 viewport,
2365 status_appearance.text_color,
2366 Rectangle {
2367 width,
2368 height: width,
2369 ..image_bounds
2370 },
2371 icon.clone(),
2372 );
2373
2374 bounds.x += offset;
2375 } else {
2376 if key_is_active && let crate::theme::SegmentedButton::Control = self.style {
2378 let mut image_bounds = bounds;
2379 image_bounds.y = center_y - 8.0;
2380
2381 draw_icon::<Message>(
2382 renderer,
2383 theme,
2384 style,
2385 cursor,
2386 viewport,
2387 status_appearance.text_color,
2388 Rectangle {
2389 width: 16.0,
2390 height: 16.0,
2391 ..image_bounds
2392 },
2393 crate::widget::icon(match crate::widget::common::object_select().data() {
2394 iced_core::svg::Data::Bytes(bytes) => {
2395 crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true)
2396 }
2397 iced_core::svg::Data::Path(path) => {
2398 crate::widget::icon::from_path(path.clone())
2399 }
2400 }),
2401 );
2402
2403 let offset = 16.0 + f32::from(self.button_spacing);
2404
2405 bounds.x += offset;
2406 }
2407 }
2408
2409 let show_close_button =
2411 (key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
2412 && self.model.is_closable(key);
2413
2414 let close_icon_width = if show_close_button {
2416 f32::from(self.close_icon.size)
2417 } else {
2418 0.0
2419 };
2420
2421 bounds.width = original_bounds.width
2422 - (bounds.x - original_bounds.x)
2423 - close_icon_width
2424 - f32::from(self.button_padding[2]);
2425
2426 bounds.y = center_y;
2427
2428 if self.model.text(key).is_some_and(|text| !text.is_empty()) {
2429 bounds.y -= state.paragraphs[key].min_height() / 2.;
2431
2432 renderer.fill_paragraph(
2434 state.paragraphs[key].raw(),
2435 bounds.position(),
2436 status_appearance.text_color,
2437 Rectangle {
2438 x: bounds.x,
2439 width: bounds.width,
2440 height: original_bounds.height,
2441 y: bounds.y,
2442 },
2444 );
2445 }
2446
2447 if show_close_button {
2449 let close_button_bounds = close_bounds(original_bounds, close_icon_width);
2450
2451 draw_icon::<Message>(
2452 renderer,
2453 theme,
2454 style,
2455 cursor,
2456 viewport,
2457 status_appearance.text_color,
2458 close_button_bounds,
2459 self.close_icon.clone(),
2460 );
2461 }
2462
2463 if show_drop_hint_marker {
2464 if matches!(
2465 drop_hint_marker,
2466 Some(DropHint {
2467 entity,
2468 side: DropSide::After
2469 }) if entity == key
2470 ) {
2471 draw_drop_indicator(
2472 renderer,
2473 original_bounds,
2474 DropSide::After,
2475 Self::VERTICAL,
2476 appearance.active.text_color,
2477 );
2478 }
2479 }
2480
2481 nth += 1;
2482 });
2483 }
2484
2485 fn overlay<'b>(
2486 &'b mut self,
2487 tree: &'b mut Tree,
2488 layout: iced_core::Layout<'b>,
2489 _renderer: &Renderer,
2490 _viewport: &iced_core::Rectangle,
2491 translation: Vector,
2492 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
2493 #[cfg(wayland_platform)]
2494 if matches!(
2495 crate::app::cosmic::WINDOWING_SYSTEM.get(),
2496 Some(crate::app::cosmic::WindowingSystem::Wayland)
2497 ) && self.on_surface_action.is_some()
2498 && self.window_id != window::Id::NONE
2499 {
2500 return None;
2501 }
2502
2503 let state = tree.state.downcast_mut::<LocalState>();
2504 let menu_state = state.menu_state.clone();
2505
2506 let entity = state.show_context?;
2507
2508 let (mut bounds, i) = self
2509 .variant_bounds(state, layout.bounds())
2510 .filter_map(|item| match item {
2511 ItemBounds::Button(entity, bounds) => Some((bounds, entity)),
2512 _ => None,
2513 })
2514 .enumerate()
2515 .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })?;
2516
2517 assert!(
2518 self.context_menu
2519 .as_ref()
2520 .is_none_or(|m| m[0].children.len() == self.model.len())
2521 );
2522 let menu = self
2523 .context_menu
2524 .as_mut()
2525 .map(|m| m[0].children[i].clone())?;
2526
2527 if !menu_state.inner.with_data(|data| data.open) {
2528 state.show_context = None;
2532 for key in self.model.order.iter().copied() {
2533 self.update_entity_paragraph(state, key);
2534 }
2535 return None;
2536 }
2537 bounds.x = state.context_cursor.x;
2538 bounds.y = state.context_cursor.y;
2539
2540 Some(
2541 crate::widget::menu::Menu {
2542 tree: menu_state,
2543 menu_roots: std::borrow::Cow::Owned(vec![menu]),
2544 bounds_expand: 16,
2545 menu_overlays_parent: true,
2546 close_condition: CloseCondition {
2547 leave: false,
2548 click_outside: true,
2549 click_inside: true,
2550 },
2551 item_width: ItemWidth::Uniform(240),
2552 item_height: ItemHeight::Dynamic(40),
2553 bar_bounds: bounds,
2554 main_offset: -bounds.height as i32,
2555 cross_offset: 0,
2556 root_bounds_list: vec![bounds],
2557 path_highlight: Some(PathHighlight::MenuActive),
2558 style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
2559 position: Point::new(translation.x, translation.y),
2560 is_overlay: true,
2561 window_id: window::Id::NONE,
2562 depth: 0,
2563 on_surface_action: None,
2564 }
2565 .overlay(),
2566 )
2567 }
2568
2569 fn drag_destinations(
2570 &self,
2571 tree: &Tree,
2572 layout: Layout<'_>,
2573 _renderer: &Renderer,
2574 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
2575 ) {
2576 let local_state = tree.state.downcast_ref::<LocalState>();
2577 let my_id = self.get_drag_id();
2578 let mut pushed = false;
2579
2580 for item in self.variant_bounds(local_state, layout.bounds()) {
2581 if let ItemBounds::Button(_entity, rect) = item {
2582 pushed = true;
2583 log::trace!(
2584 target: TAB_REORDER_LOG_TARGET,
2585 "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2586 my_id,
2587 rect.x,
2588 rect.y,
2589 rect.width,
2590 rect.height,
2591 self.mimes
2592 );
2593 dnd_rectangles.push(DndDestinationRectangle {
2594 id: my_id,
2595 rectangle: dnd::Rectangle {
2596 x: f64::from(rect.x),
2597 y: f64::from(rect.y),
2598 width: f64::from(rect.width),
2599 height: f64::from(rect.height),
2600 },
2601 mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2602 actions: DndAction::Copy | DndAction::Move,
2603 preferred: DndAction::Move,
2604 });
2605 }
2606 }
2607
2608 if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) {
2609 for item in self.variant_bounds(local_state, layout.bounds()) {
2610 if let ItemBounds::Button(_entity, rect) = item {
2611 pushed = true;
2612 log::trace!(
2613 target: TAB_REORDER_LOG_TARGET,
2614 "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2615 my_id,
2616 rect.x,
2617 rect.y,
2618 rect.width,
2619 rect.height,
2620 mime
2621 );
2622 dnd_rectangles.push(DndDestinationRectangle {
2623 id: my_id,
2624 rectangle: dnd::Rectangle {
2625 x: f64::from(rect.x),
2626 y: f64::from(rect.y),
2627 width: f64::from(rect.width),
2628 height: f64::from(rect.height),
2629 },
2630 mime_types: vec![Cow::Owned(mime.clone())],
2631 actions: DndAction::Copy | DndAction::Move,
2632 preferred: DndAction::Move,
2633 });
2634 }
2635 }
2636 }
2637
2638 if !pushed {
2639 let bounds = layout.bounds();
2640 log::trace!(
2641 target: TAB_REORDER_LOG_TARGET,
2642 "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}",
2643 my_id,
2644 bounds.x,
2645 bounds.y,
2646 bounds.width,
2647 bounds.height,
2648 self.mimes
2649 );
2650 dnd_rectangles.push(DndDestinationRectangle {
2651 id: my_id,
2652 rectangle: dnd::Rectangle {
2653 x: f64::from(bounds.x),
2654 y: f64::from(bounds.y),
2655 width: f64::from(bounds.width),
2656 height: f64::from(bounds.height),
2657 },
2658 mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(),
2659 actions: DndAction::Copy | DndAction::Move,
2660 preferred: DndAction::Move,
2661 });
2662 }
2663 }
2664}
2665
2666impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
2667 for Element<'a, Message>
2668where
2669 SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
2670 Variant: 'static,
2671 Model<SelectionMode>: Selectable,
2672 SelectionMode: Default,
2673 Message: 'static + Clone,
2674{
2675 fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self {
2676 if widget.model.items.is_empty() {
2677 widget.spacing = 0;
2678 }
2679
2680 Self::new(widget)
2681 }
2682}
2683
2684struct TabDragSource<Message> {
2685 mime: String,
2686 threshold: f32,
2687 _marker: PhantomData<Message>,
2688}
2689
2690impl<Message> TabDragSource<Message> {
2691 fn new(mime: String) -> Self {
2692 Self {
2693 mime,
2694 threshold: 8.0,
2695 _marker: PhantomData,
2696 }
2697 }
2698}
2699
2700struct SimpleDragData {
2701 mime: String,
2702 bytes: Vec<u8>,
2703}
2704
2705impl SimpleDragData {
2706 fn new(mime: String, bytes: Vec<u8>) -> Self {
2707 Self { mime, bytes }
2708 }
2709}
2710
2711impl iced::clipboard::mime::AsMimeTypes for SimpleDragData {
2712 fn available(&self) -> Cow<'static, [String]> {
2713 Cow::Owned(vec![self.mime.clone()])
2714 }
2715
2716 fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
2717 if mime_type == self.mime {
2718 Some(Cow::Owned(self.bytes.clone()))
2719 } else {
2720 None
2721 }
2722 }
2723}
2724
2725#[derive(Clone, Copy)]
2726struct TabDragCandidate {
2727 entity: Entity,
2728 bounds: Rectangle,
2729 origin: Point,
2730}
2731
2732#[derive(Debug, Clone, Copy)]
2733struct Focus {
2734 updated_at: Instant,
2735 now: Instant,
2736}
2737
2738pub struct LocalState {
2740 pub(crate) menu_state: MenuBarState,
2742 pub(super) buttons_visible: usize,
2744 pub(super) buttons_offset: usize,
2746 pub(super) collapsed: bool,
2748 focused_visible: bool,
2750 focused: Option<Focus>,
2752 focused_item: Item,
2754 hovered: Item,
2756 middle_clicked: Option<Item>,
2758 pub(super) known_length: usize,
2760 pub(super) internal_layout: Vec<(Size, Size)>,
2762 paragraphs: SecondaryMap<Entity, crate::Plain>,
2764 text_hashes: SecondaryMap<Entity, u64>,
2766 context_cursor: Point,
2768 show_context: Option<Entity>,
2770 wheel_timestamp: Option<Instant>,
2772 pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
2774 pub offer_mimes: Vec<String>,
2776 fingers_pressed: HashSet<Finger>,
2778 pressed_item: Option<Item>,
2780 tab_drag_candidate: Option<TabDragCandidate>,
2782 dragging_tab: Option<Entity>,
2784 drop_hint: Option<DropHint>,
2786}
2787
2788#[derive(Clone, Copy, Debug, Default, PartialEq)]
2789enum Item {
2790 NextButton,
2791 #[default]
2792 None,
2793 PrevButton,
2794 Set,
2795 Tab(Entity),
2796}
2797
2798impl LocalState {
2799 fn set_focused(&mut self) {
2800 let now = Instant::now();
2801 LAST_FOCUS_UPDATE.with(|x| x.set(now));
2802
2803 self.focused = Some(Focus {
2804 updated_at: now,
2805 now,
2806 });
2807 }
2808}
2809
2810#[cfg(test)]
2811mod tests {
2812 use super::*;
2813 use crate::widget::segmented_button::{self, Appearance as SegAppearance};
2814 use iced::Size;
2815 use slotmap::SecondaryMap;
2816 use std::collections::HashSet;
2817
2818 #[derive(Clone, Debug)]
2819 enum TestMessage {}
2820
2821 struct TestVariant;
2822
2823 impl<SelectionMode, Message> SegmentedVariant
2824 for SegmentedButton<'_, TestVariant, SelectionMode, Message>
2825 where
2826 Model<SelectionMode>: Selectable,
2827 SelectionMode: Default,
2828 Message: Clone,
2829 {
2830 const VERTICAL: bool = false;
2831
2832 fn variant_appearance(
2833 _theme: &crate::Theme,
2834 _style: &crate::theme::SegmentedButton,
2835 ) -> SegAppearance {
2836 SegAppearance::default()
2837 }
2838
2839 fn variant_bounds<'b>(
2840 &'b self,
2841 _state: &'b LocalState,
2842 bounds: Rectangle,
2843 ) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
2844 let len = self.model.order.len();
2845 if len == 0 {
2846 return Box::new(std::iter::empty());
2847 }
2848 let width = bounds.width / len as f32;
2849 Box::new(
2850 self.model
2851 .order
2852 .iter()
2853 .copied()
2854 .enumerate()
2855 .map(move |(idx, entity)| {
2856 let rect = Rectangle {
2857 x: bounds.x + (idx as f32) * width,
2858 y: bounds.y,
2859 width,
2860 height: bounds.height,
2861 };
2862 ItemBounds::Button(entity, rect)
2863 }),
2864 )
2865 }
2866
2867 fn variant_layout(
2868 &self,
2869 _state: &mut LocalState,
2870 _renderer: &crate::Renderer,
2871 _limits: &layout::Limits,
2872 ) -> Size {
2873 Size::ZERO
2874 }
2875 }
2876
2877 fn sample_model() -> (
2878 segmented_button::SingleSelectModel,
2879 Vec<segmented_button::Entity>,
2880 ) {
2881 let mut entities = Vec::new();
2882 let model = segmented_button::Model::builder()
2883 .insert(|b| b.text("One").with_id(|id| entities.push(id)))
2884 .insert(|b| b.text("Two").with_id(|id| entities.push(id)))
2885 .insert(|b| b.text("Three").with_id(|id| entities.push(id)))
2886 .build();
2887 (model, entities)
2888 }
2889
2890 fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState {
2891 let mut state = LocalState {
2892 menu_state: MenuBarState::default(),
2893 paragraphs: SecondaryMap::new(),
2894 text_hashes: SecondaryMap::new(),
2895 buttons_visible: 0,
2896 buttons_offset: 0,
2897 collapsed: false,
2898 focused: None,
2899 focused_item: Item::default(),
2900 focused_visible: false,
2901 hovered: Item::default(),
2902 known_length: 0,
2903 middle_clicked: None,
2904 internal_layout: Vec::new(),
2905 context_cursor: Point::ORIGIN,
2906 show_context: None,
2907 wheel_timestamp: None,
2908 dnd_state: crate::widget::dnd_destination::State::<Option<Entity>>::new(),
2909 fingers_pressed: HashSet::new(),
2910 pressed_item: None,
2911 tab_drag_candidate: None,
2912 dragging_tab: Some(dragging),
2913 drop_hint: None,
2914 offer_mimes: Vec::new(),
2915 };
2916 state.buttons_visible = len;
2917 state.known_length = len;
2918 state
2919 }
2920
2921 #[test]
2922 fn drop_hint_reports_before_and_after() {
2923 let (model, ids) = sample_model();
2924 let button =
2925 SegmentedButton::<TestVariant, segmented_button::SingleSelect, TestMessage>::new(
2926 &model,
2927 );
2928 let state = test_state(ids[0], model.order.len());
2929 let bounds = Rectangle {
2930 x: 0.0,
2931 y: 0.0,
2932 width: 300.0,
2933 height: 30.0,
2934 };
2935 let before = button
2936 .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0))
2937 .expect("hint");
2938 assert_eq!(before.entity, ids[0]);
2939 assert!(matches!(before.side, DropSide::Before));
2940
2941 let after = button
2942 .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0))
2943 .expect("hint");
2944 assert_eq!(after.entity, ids[2]);
2945 assert!(matches!(after.side, DropSide::After));
2946 }
2947}
2948
2949impl operation::Focusable for LocalState {
2950 fn is_focused(&self) -> bool {
2951 self.focused
2952 .is_some_and(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()))
2953 }
2954
2955 fn focus(&mut self) {
2956 self.set_focused();
2957 self.focused_visible = true;
2958 self.focused_item = Item::Set;
2959 }
2960
2961 fn unfocus(&mut self) {
2962 self.focused = None;
2963 self.focused_item = Item::None;
2964 self.focused_visible = false;
2965 self.show_context = None;
2966 }
2967}
2968
2969#[derive(Debug, Clone, PartialEq)]
2971pub struct Id(widget::Id);
2972
2973impl Id {
2974 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
2976 Self(widget::Id::new(id))
2977 }
2978
2979 #[must_use]
2983 #[inline]
2984 pub fn unique() -> Self {
2985 Self(widget::Id::unique())
2986 }
2987}
2988
2989impl From<Id> for widget::Id {
2990 fn from(id: Id) -> Self {
2991 id.0
2992 }
2993}
2994
2995fn close_bounds(area: Rectangle<f32>, icon_size: f32) -> Rectangle<f32> {
2997 Rectangle {
2998 x: area.x + area.width - icon_size - 8.0,
2999 y: area.center_y() - (icon_size / 2.0),
3000 width: icon_size,
3001 height: icon_size,
3002 }
3003}
3004
3005fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
3007 Rectangle {
3008 x: bounds.x + bounds.width - button_height,
3009 y: bounds.y,
3010 width: button_height,
3011 height: button_height,
3012 }
3013}
3014
3015fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle {
3017 Rectangle {
3018 x: bounds.x,
3019 y: bounds.y,
3020 width: button_height,
3021 height: button_height,
3022 }
3023}
3024
3025#[allow(clippy::too_many_arguments)]
3026fn draw_icon<Message: 'static>(
3027 renderer: &mut Renderer,
3028 theme: &crate::Theme,
3029 style: &renderer::Style,
3030 cursor: mouse::Cursor,
3031 viewport: &Rectangle,
3032 color: Color,
3033 bounds: Rectangle,
3034 icon: Icon,
3035) {
3036 let layout_node = layout::Node::new(Size {
3037 width: bounds.width,
3038 height: bounds.width,
3039 })
3040 .move_to(Point {
3041 x: bounds.x,
3042 y: bounds.y,
3043 });
3044
3045 Widget::<Message, crate::Theme, Renderer>::draw(
3046 Element::<Message>::from(icon).as_widget(),
3047 &Tree::empty(),
3048 renderer,
3049 theme,
3050 &renderer::Style {
3051 icon_color: color,
3052 text_color: color,
3053 scale_factor: style.scale_factor,
3054 },
3055 Layout::new(&layout_node),
3056 cursor,
3057 viewport,
3058 );
3059}
3060
3061fn draw_drop_indicator(
3062 renderer: &mut Renderer,
3063 bounds: Rectangle,
3064 side: DropSide,
3065 vertical: bool,
3066 color: Color,
3067) {
3068 let thickness = 4.0;
3069 let quad_bounds = if vertical {
3070 let y = match side {
3071 DropSide::Before => bounds.y - thickness / 2.0,
3072 DropSide::After => bounds.y + bounds.height - thickness / 2.0,
3073 };
3074
3075 Rectangle {
3076 x: bounds.x,
3077 y,
3078 width: bounds.width,
3079 height: thickness,
3080 }
3081 } else {
3082 let x = match side {
3083 DropSide::Before => bounds.x - thickness / 2.0,
3084 DropSide::After => bounds.x + bounds.width - thickness / 2.0,
3085 };
3086
3087 Rectangle {
3088 x,
3089 y: bounds.y,
3090 width: thickness,
3091 height: bounds.height,
3092 }
3093 };
3094
3095 renderer.fill_quad(
3096 renderer::Quad {
3097 bounds: quad_bounds,
3098 border: Border {
3099 radius: 2.0.into(),
3100 ..Default::default()
3101 },
3102 shadow: Shadow::default(),
3103 snap: true,
3104 },
3105 Background::Color(color),
3106 );
3107}
3108
3109fn left_button_released(event: &Event) -> bool {
3110 matches!(
3111 event,
3112 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
3113 )
3114}
3115
3116fn right_button_released(event: &Event) -> bool {
3117 matches!(
3118 event,
3119 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,))
3120 )
3121}
3122
3123fn is_pressed(event: &Event) -> bool {
3124 matches!(
3125 event,
3126 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
3127 | Event::Touch(touch::Event::FingerPressed { .. })
3128 )
3129}
3130
3131fn is_lifted(event: &Event) -> bool {
3132 matches!(
3133 event,
3134 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
3135 | Event::Touch(touch::Event::FingerLifted { .. })
3136 )
3137}
3138
3139fn touch_lifted(event: &Event) -> bool {
3140 matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
3141}