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