// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 use super::model::{Entity, Model, Selectable}; use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::{icon, Icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::{ alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Command, Event, Length, Padding, Rectangle, Size, }; use iced_core::mouse::ScrollDelta; use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; use iced_core::widget::{self, operation, tree}; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; use iced_core::{Border, Gradient, Point, Renderer as IcedRenderer, Shadow, Text}; use slotmap::{Key, SecondaryMap}; use std::marker::PhantomData; use std::time::{Duration, Instant}; /// A command that focuses a segmented item stored in a widget. pub fn focus(id: Id) -> Command { Command::widget(operation::focusable::focus(id.0)) } pub enum ItemBounds { Button(Entity, Rectangle), Divider(Rectangle), } /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { /// Get the appearance for this variant of the widget. fn variant_appearance( theme: &crate::Theme, style: &crate::theme::SegmentedButton, ) -> super::Appearance; /// Calculates the bounds for visible buttons. fn variant_bounds<'b>( &'b self, state: &'b LocalState, bounds: Rectangle, ) -> Box + 'b>; /// Calculates the layout of this variant. fn variant_layout( &self, state: &mut LocalState, renderer: &crate::Renderer, limits: &layout::Limits, ) -> Size; } /// A conjoined group of items that function together as a button. #[derive(Setters)] #[must_use] pub struct SegmentedButton<'a, Variant, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, { /// The model borrowed from the application create this widget. #[setters(skip)] pub(super) model: &'a Model, /// iced widget ID pub(super) id: Option, /// The icon used for the close button. pub(super) close_icon: Icon, /// Scrolling switches focus between tabs. pub(super) scrollable_focus: bool, /// Show the close icon only when item is hovered. pub(super) show_close_icon_on_hover: bool, /// Padding of the whole widget. #[setters(into)] pub(super) padding: Padding, /// Whether to place dividers between buttons. pub(super) dividers: bool, /// Alignment of button contents. pub(super) button_alignment: Alignment, /// Padding around a button. pub(super) button_padding: [u16; 4], /// Desired height of a button. pub(super) button_height: u16, /// Spacing between icon and text in button. pub(super) button_spacing: u16, /// Minimum width of a button. pub(super) minimum_button_width: u16, /// Spacing for each indent. pub(super) indent_spacing: u16, /// Desired font for active tabs. pub(super) font_active: Option, /// Desired font for hovered tabs. pub(super) font_hovered: Option, /// Desired font for inactive tabs. pub(super) font_inactive: Option, /// Size of the font. pub(super) font_size: f32, /// Desired width of the widget. pub(super) width: Length, /// Desired height of the widget. pub(super) height: Length, /// Desired spacing between items. pub(super) spacing: u16, /// LineHeight of the font. pub(super) line_height: LineHeight, /// Style to draw the widget in. #[setters(into)] pub(super) style: Style, /// Emits the ID of the item that was activated. #[setters(skip)] pub(super) on_activate: Option Message + 'static>>, #[setters(skip)] pub(super) on_close: Option Message + 'static>>, #[setters(skip)] /// Defines the implementation of this struct variant: PhantomData, } impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message> where Self: SegmentedVariant, Model: Selectable, SelectionMode: Default, { pub fn new(model: &'a Model) -> Self { Self { model, id: None, close_icon: icon::from_name("window-close-symbolic").size(16).icon(), scrollable_focus: false, show_close_icon_on_hover: false, button_alignment: Alignment::Start, padding: Padding::from(0.0), dividers: false, button_padding: [0, 0, 0, 0], button_height: 32, button_spacing: 0, minimum_button_width: 150, indent_spacing: 16, font_active: None, font_hovered: None, font_inactive: None, font_size: 14.0, height: Length::Shrink, width: Length::Fill, spacing: 0, line_height: LineHeight::default(), style: Style::default(), on_activate: None, on_close: None, variant: PhantomData, } } /// Emitted when a tab is pressed. pub fn on_activate(mut self, on_activate: T) -> Self where T: Fn(Entity) -> Message + 'static, { self.on_activate = Some(Box::new(on_activate)); self } /// Emitted when a tab close button is pressed. pub fn on_close(mut self, on_close: T) -> Self where T: Fn(Entity) -> Message + 'static, { self.on_close = Some(Box::new(on_close)); self } /// Check if an item is enabled. fn is_enabled(&self, key: Entity) -> bool { self.model.items.get(key).map_or(false, |item| item.enabled) } /// Item the previous item in the widget. fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state).rev(); while let Some(key) = keys.next() { if key == entity { for key in keys { // Skip disabled buttons. if !self.is_enabled(key) { continue; } state.focused_item = Item::Tab(key); return event::Status::Captured; } break; } } if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; return event::Status::Captured; } } Item::NextButton => { if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); return event::Status::Captured; } } Item::None => { if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; return event::Status::Captured; } else if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); return event::Status::Captured; } } Item::PrevButton | Item::Set => (), } state.focused_item = Item::None; event::Status::Ignored } /// Item the next item in the widget. fn focus_next(&mut self, state: &mut LocalState) -> event::Status { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state); while let Some(key) = keys.next() { if key == entity { for key in keys { // Skip disabled buttons. if !self.is_enabled(key) { continue; } state.focused_item = Item::Tab(key); return event::Status::Captured; } break; } } if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; return event::Status::Captured; } } Item::PrevButton => { if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); return event::Status::Captured; } } Item::None => { if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; return event::Status::Captured; } else if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); return event::Status::Captured; } } Item::NextButton | Item::Set => (), } state.focused_item = Item::None; event::Status::Ignored } fn iterate_visible_tabs<'b>( &'b self, state: &LocalState, ) -> impl DoubleEndedIterator + 'b { self.model .order .iter() .copied() .skip(state.buttons_offset) .take(state.buttons_visible) } fn first_tab(&self, state: &LocalState) -> Option { self.model.order.get(state.buttons_offset).copied() } fn last_tab(&self, state: &LocalState) -> Option { self.model .order .get(state.buttons_offset + state.buttons_visible) .copied() } #[allow(clippy::unused_self)] fn prev_tab_sensitive(&self, state: &LocalState) -> bool { state.buttons_offset > 0 } fn next_tab_sensitive(&self, state: &LocalState) -> bool { state.buttons_offset < self.model.order.len() - state.buttons_visible } pub(super) fn button_dimensions( &self, state: &mut LocalState, font: crate::font::Font, button: Entity, ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; // Add text to measurement if text was given. if let Some((text, entry)) = self .model .text .get(button) .zip(state.paragraphs.entry(button)) { let paragraph = entry.or_insert_with(|| { crate::Paragraph::with_text(Text { content: text, size: iced::Pixels(self.font_size), bounds: Size::INFINITY, font, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, line_height: self.line_height, }) }); let size = paragraph.min_bounds(); width += size.width; height += size.height; } // Add indent to measurement if found. if let Some(indent) = self.model.indent(button) { width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width); } // Add icon to measurement if icon was given. if let Some(icon) = self.model.icon(button) { width += f32::from(icon.size) + f32::from(self.button_spacing); } else if self.model.is_active(button) { // Add selection icon measurements when widget is a selection widget. if let crate::theme::SegmentedButton::Control = self.style { width += 16.0 + f32::from(self.button_spacing); } } // Add close button to measurement if found. if self.model.is_closable(button) { width += f32::from(self.close_icon.size) + f32::from(self.button_spacing); } // Add button padding to the max size found width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); height += f32::from(self.button_padding[1]) + f32::from(self.button_padding[3]); height = height.max(f32::from(self.button_height)); (width, height) } pub(super) fn max_button_dimensions( &self, state: &mut LocalState, renderer: &Renderer, ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; let font = renderer.default_font(); for key in self.model.order.iter().copied() { let (button_width, button_height) = self.button_dimensions(state, font, key); state.internal_layout.push(( Size::new(button_width, button_height), Size::new( button_width - f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]), button_height, ), )); height = height.max(button_height); width = width.max(button_width); } for (size, actual) in &mut state.internal_layout { size.height = height; actual.height = height; } (width, height) } } impl<'a, Variant, SelectionMode, Message> Widget for SegmentedButton<'a, Variant, SelectionMode, Message> where Self: SegmentedVariant, Model: Selectable, SelectionMode: Default, Message: 'static + Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(LocalState { paragraphs: SecondaryMap::new(), ..LocalState::default() }) } fn diff(&mut self, tree: &mut Tree) { for e in self.model.order.iter().copied() { if let Some(text) = self.model.text.get(e) { let text = Text { content: text, size: iced::Pixels(self.font_size), bounds: Size::INFINITY, font: self.font_active.unwrap_or(crate::font::FONT), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, line_height: self.line_height, }; if let Some(paragraph) = tree .state .downcast_mut::() .paragraphs .get_mut(e) { paragraph.update(text); } else { tree.state .downcast_mut::() .paragraphs .insert(e, crate::Paragraph::with_text(text)); } } } } fn size(&self) -> Size { Size::new(self.width, self.height) } fn layout( &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let state = tree.state.downcast_mut::(); let limits = limits.shrink(self.padding); let size = self .variant_layout(state, renderer, &limits) .expand(self.padding); layout::Node::new(size) } #[allow(clippy::too_many_lines)] fn on_event( &mut self, tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, ) -> event::Status { let bounds = layout.bounds(); let state = tree.state.downcast_mut::(); state.hovered = Item::None; if cursor_position.is_over(bounds) { // Check for clicks on the previous and next tab buttons, when tabs are collapsed. if state.collapsed { // Check if the prev tab button was clicked. if cursor_position.is_over(prev_tab_bounds(&bounds, f32::from(self.button_height))) && self.prev_tab_sensitive(state) { state.hovered = Item::PrevButton; if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { state.buttons_offset -= 1; } } else { // Check if the next tab button was clicked. if cursor_position .is_over(next_tab_bounds(&bounds, f32::from(self.button_height))) && self.next_tab_sensitive(state) { state.hovered = Item::NextButton; if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { state.buttons_offset += 1; } } } } for (key, bounds) in self .variant_bounds(state, bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, }) .collect::>() { if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. state.hovered = Item::Tab(key); // If marked as closable, show a close icon. if self.model.items[key].closable { // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { if cursor_position .is_over(close_bounds(bounds, f32::from(self.close_icon.size))) { if let Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Left, )) | Event::Touch(touch::Event::FingerLifted { .. }) = event { shell.publish(on_close(key)); return event::Status::Captured; } } // Emit close message if the tab is middle clicked. if let Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Middle, )) = event { shell.publish(on_close(key)); return event::Status::Captured; } } } if let Some(on_activate) = self.on_activate.as_ref() { if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { shell.publish(on_activate(key)); return event::Status::Captured; } } } break; } } if self.scrollable_focus { if let Some(on_activate) = self.on_activate.as_ref() { if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { let current = Instant::now(); // Permit successive scroll wheel events only after a given delay. if state.wheel_timestamp.map_or(true, |previous| { current.duration_since(previous) > Duration::from_millis(250) }) { state.wheel_timestamp = Some(current); match delta { ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { let mut activate_key = None; if y < 0.0 { let mut prev_key = Entity::null(); for key in self.model.order.iter().copied() { if self.model.is_active(key) && !prev_key.is_null() { activate_key = Some(prev_key); } if self.model.is_enabled(key) { prev_key = key; } } } else if y > 0.0 { let mut buttons = self.model.order.iter().copied(); while let Some(key) = buttons.next() { if self.model.is_active(key) { for key in buttons { if self.model.is_enabled(key) { activate_key = Some(key); break; } } break; } } } if let Some(key) = activate_key { shell.publish(on_activate(key)); return event::Status::Captured; } } } } } } } } if state.focused { if let Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Tab), modifiers, .. }) = event { return if modifiers.shift() { self.focus_previous(state) } else { self.focus_next(state) }; } if let Some(on_activate) = self.on_activate.as_ref() { if let Event::Keyboard(keyboard::Event::KeyReleased { key: keyboard::Key::Named(keyboard::key::Named::Enter), .. }) = event { match state.focused_item { Item::Tab(entity) => { shell.publish(on_activate(entity)); } Item::PrevButton => { if self.prev_tab_sensitive(state) { state.buttons_offset -= 1; // If the change would cause it to be insensitive, focus the first tab. if !self.prev_tab_sensitive(state) { if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); } } } } Item::NextButton => { if self.next_tab_sensitive(state) { state.buttons_offset += 1; // If the change would cause it to be insensitive, focus the last tab. if !self.next_tab_sensitive(state) { if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); } } } } Item::None | Item::Set => (), } return event::Status::Captured; } } } event::Status::Ignored } fn operate( &self, tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation< iced_core::widget::OperationOutputWrapper, >, ) { let state = tree.state.downcast_mut::(); operation.focusable(state, self.id.as_ref().map(|id| &id.0)); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; } else if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); } } } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor_position: mouse::Cursor, _viewport: &iced::Rectangle, _renderer: &Renderer, ) -> iced_core::mouse::Interaction { if self.on_activate.is_none() { return iced_core::mouse::Interaction::default(); } let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); if cursor_position.is_over(bounds) { let hovered_button = self .variant_bounds(state, bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, }) .find(|(_key, bounds)| cursor_position.is_over(*bounds)); if let Some((key, _bounds)) = hovered_button { return if self.model.items[key].enabled { iced_core::mouse::Interaction::Pointer } else { iced_core::mouse::Interaction::Idle }; } } iced_core::mouse::Interaction::Idle } #[allow(clippy::too_many_lines)] fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &crate::Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &iced::Rectangle, ) { let state = tree.state.downcast_ref::(); let appearance = Self::variant_appearance(theme, &self.style); let bounds: Rectangle = layout.bounds(); let button_amount = self.model.items.len(); // Modifies alpha color when `on_activate` is unset. let apply_alpha = |mut c: Color| { if self.on_activate.is_none() { c.a /= 2.0; } c }; // Maps `apply_alpha` to background color. let bg_with_alpha = |mut b| { match &mut b { Background::Color(c) => { *c = apply_alpha(*c); } Background::Gradient(g) => { let Gradient::Linear(mut l) = g; for c in &mut l.stops { let Some(stop) = c else { continue; }; stop.color = apply_alpha(stop.color); } } } b }; // Draw the background, if a background was defined. if let Some(background) = appearance.background { renderer.fill_quad( renderer::Quad { bounds, border: Border { radius: appearance.border_radius, ..Border::default() }, shadow: Shadow::default(), }, bg_with_alpha(background), ); } // Draw previous and next tab buttons if there is a need to paginate tabs. if state.collapsed { let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height)); // Previous tab button let mut background_appearance = if self.on_activate.is_some() && Item::PrevButton == state.focused_item { Some(appearance.focus) } else if self.on_activate.is_some() && Item::PrevButton == state.hovered { Some(appearance.hover) } else { None }; if let Some(background_appearance) = background_appearance.take() { renderer.fill_quad( renderer::Quad { bounds: tab_bounds, border: Border { radius: theme.cosmic().radius_s().into(), ..Default::default() }, shadow: Shadow::default(), }, background_appearance .background .map_or(Background::Color(Color::TRANSPARENT), bg_with_alpha), ); } draw_icon::( renderer, theme, style, cursor, viewport, apply_alpha(if state.buttons_offset == 0 { appearance.inactive.text_color } else if let Item::PrevButton = state.focused_item { appearance.focus.text_color } else { appearance.active.text_color }), Rectangle { x: tab_bounds.x + 8.0, y: tab_bounds.y + f32::from(self.button_height) / 4.0, width: 16.0, height: 16.0, }, icon::from_name("go-previous-symbolic").size(16).icon(), ); tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height)); // Next tab button background_appearance = if self.on_activate.is_some() && Item::NextButton == state.focused_item { Some(appearance.focus) } else if self.on_activate.is_some() && Item::NextButton == state.hovered { Some(appearance.hover) } else { None }; if let Some(background_appearance) = background_appearance { renderer.fill_quad( renderer::Quad { bounds: tab_bounds, border: Border { radius: theme.cosmic().radius_s().into(), ..Default::default() }, shadow: Shadow::default(), }, background_appearance .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); } draw_icon::( renderer, theme, style, cursor, viewport, apply_alpha(if self.next_tab_sensitive(state) { appearance.active.text_color } else if let Item::NextButton = state.focused_item { appearance.focus.text_color } else { appearance.inactive.text_color }), // Rectangle { // x: bounds.x + bounds.width - f32::from(self.button_height) // + f32::from(self.button_height) / 4.0, // y: f32::from(self.button_height) / 2.0 - f32::from(self.button_height) / 2.0, // width: 16.0, // height: 16.0, // }, Rectangle { x: tab_bounds.x + 8.0, y: tab_bounds.y + f32::from(self.button_height) / 4.0, width: 16.0, height: 16.0, }, icon::from_name("go-next-symbolic").size(16).icon(), ); } // Draw each of the items in the widget. let mut nth = 0; self.variant_bounds(state, bounds).for_each(move |item| { let (key, mut bounds) = match item { // Draw a button ItemBounds::Button(entity, bounds) => (entity, bounds), // Draw a divider between buttons ItemBounds::Divider(bounds) => { renderer.fill_quad( renderer::Quad { bounds, border: Border::default(), shadow: Shadow::default(), }, { let theme = crate::theme::active(); Background::Color(theme.cosmic().small_widget_divider().into()) }, ); return; } }; let center_y = bounds.center_y(); let key_is_active = self.model.is_active(key); let key_is_hovered = self.on_activate.is_some() && state.hovered == Item::Tab(key); let (status_appearance, font) = if self.on_activate.is_some() && Item::Tab(key) == state.focused_item { (appearance.focus, &self.font_active) } else if key_is_active { (appearance.active, &self.font_active) } else if key_is_hovered { (appearance.hover, &self.font_hovered) } else { (appearance.inactive, &self.font_inactive) }; let font = font.unwrap_or_else(|| renderer.default_font()); let button_appearance = if nth == 0 { status_appearance.first } else if nth + 1 == button_amount { status_appearance.last } else { status_appearance.middle }; // Render the background of the button. if status_appearance.background.is_some() { renderer.fill_quad( renderer::Quad { bounds, border: Border { radius: button_appearance.border_radius, ..Default::default() }, shadow: Shadow::default(), }, status_appearance .background .map_or(Background::Color(Color::TRANSPARENT), bg_with_alpha), ); } // Draw the bottom border defined for this button. if let Some((width, background)) = button_appearance.border_bottom { let mut bounds = bounds; bounds.y = bounds.y + bounds.height - width; bounds.height = width; let rad_0 = THEME.with(|t| t.borrow().cosmic().corner_radii.radius_0); renderer.fill_quad( renderer::Quad { bounds, border: Border { radius: rad_0.into(), ..Default::default() }, shadow: Shadow::default(), }, bg_with_alpha(background.into()), ); } let original_bounds = bounds; bounds.x += f32::from(self.button_padding[0]); bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); // Adjust bounds by indent if let Some(indent) = self.model.indent(key) { let adjustment = f32::from(indent) * f32::from(self.indent_spacing); bounds.x += adjustment; bounds.width -= adjustment; } // Align contents of the button to the requested `button_alignment`. { let actual_width = state.internal_layout[nth].1.width; let offset = match self.button_alignment { Alignment::Start => None, Alignment::Center => Some((bounds.width - actual_width) / 2.0), Alignment::End => Some(bounds.width - actual_width), }; if let Some(offset) = offset { bounds.x += offset - f32::from(self.button_padding[0]); bounds.width = actual_width; } } // Draw the image beside the text. if let Some(icon) = self.model.icon(key) { let mut image_bounds = bounds; let width = f32::from(icon.size); let offset = width + f32::from(self.button_spacing); image_bounds.y = center_y - width / 2.0; draw_icon::( renderer, theme, style, cursor, viewport, apply_alpha(status_appearance.text_color), Rectangle { width, height: width, ..image_bounds }, icon.clone(), ); bounds.x += offset; bounds.width -= offset; } else { // Draw the selection indicator if widget is a segmented selection, and the item is selected. if key_is_active { if let crate::theme::SegmentedButton::Control = self.style { let mut image_bounds = bounds; image_bounds.y = center_y - 16.0 / 2.0; draw_icon::( renderer, theme, style, cursor, viewport, apply_alpha(status_appearance.text_color), Rectangle { width: 16.0, height: 16.0, ..image_bounds }, crate::widget::icon( match crate::widget::common::object_select().data() { crate::iced_core::svg::Data::Bytes(bytes) => { crate::widget::icon::from_svg_bytes(bytes.as_ref()) } crate::iced_core::svg::Data::Path(path) => { crate::widget::icon::from_path(path.clone()) } }, ), ); let offset = 16.0 + f32::from(self.button_spacing); bounds.x += offset; bounds.width -= offset; } } } // Whether to show the close button on this tab. let show_close_button = (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) && self.model.is_closable(key); // Width of the icon used by the close button, which we will subtract from the text bounds. let close_icon_width = if show_close_button { f32::from(self.close_icon.size) } else { 0.0 }; if let Some(text) = self.model.text(key) { bounds.y = center_y; // Draw the text for this segmented button or tab. renderer.fill_text( iced_core::text::Text { content: text, size: iced::Pixels(self.font_size), bounds: bounds.size(), font, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, line_height: self.line_height, }, bounds.position(), apply_alpha(status_appearance.text_color), Rectangle { width: bounds.width - close_icon_width, ..original_bounds }, ); } // Draw a close button if set. if show_close_button { let close_button_bounds = close_bounds(original_bounds, close_icon_width); draw_icon::( renderer, theme, style, cursor, viewport, apply_alpha(status_appearance.text_color), close_button_bounds, self.close_icon.clone(), ); } nth += 1; }); } fn overlay<'b>( &'b mut self, _tree: &'b mut Tree, _layout: iced_core::Layout<'_>, _renderer: &Renderer, ) -> Option> { None } } impl<'a, Variant, SelectionMode, Message> From> for Element<'a, Message> where SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant, Variant: 'static, Model: Selectable, SelectionMode: Default, Message: 'static + Clone, { fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self { if widget.model.items.is_empty() { widget.spacing = 0; } Self::new(widget) } } /// State that is maintained by each individual widget. #[derive(Default)] pub struct LocalState { /// Defines how many buttons to show at a time. pub(super) buttons_visible: usize, /// Button visibility offset, when collapsed. pub(super) buttons_offset: usize, /// Whether buttons need to be collapsed to preserve minimum width pub(super) collapsed: bool, /// If the widget is focused or not. focused: bool, /// The key inside the widget that is currently focused. focused_item: Item, /// The ID of the button that is being hovered. Defaults to null. hovered: Item, /// Last known length of the model. pub(super) known_length: usize, /// Dimensions of internal buttons when shrinking pub(super) internal_layout: Vec<(Size, Size)>, /// The paragraphs for each text. paragraphs: SecondaryMap, /// Time since last tab activation from wheel movements. wheel_timestamp: Option, } #[derive(Debug, Default, PartialEq)] enum Item { NextButton, #[default] None, PrevButton, Set, Tab(Entity), } impl operation::Focusable for LocalState { fn is_focused(&self) -> bool { self.focused } fn focus(&mut self) { self.focused = true; self.focused_item = Item::Set; } fn unfocus(&mut self) { self.focused = false; self.focused_item = Item::None; } } /// The iced identifier of a segmented button. #[derive(Debug, Clone, PartialEq)] pub struct Id(widget::Id); impl Id { /// Creates a custom [`Id`]. pub fn new(id: impl Into>) -> Self { Self(widget::Id::new(id)) } /// Creates a unique [`Id`]. /// /// This function produces a different [`Id`] every time it is called. #[must_use] pub fn unique() -> Self { Self(widget::Id::unique()) } } impl From for widget::Id { fn from(id: Id) -> Self { id.0 } } /// Calculates the bounds of the close button within the area of an item. fn close_bounds(area: Rectangle, icon_size: f32) -> Rectangle { Rectangle { x: area.x + area.width - icon_size - 8.0, y: area.center_y() - (icon_size / 2.0), width: icon_size, height: icon_size, } } /// Calculate the bounds of the `next_tab` button. fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle { Rectangle { x: bounds.x + bounds.width - button_height, y: bounds.y + button_height / 4.0, width: button_height, height: button_height, } } /// Calculate the bounds of the `prev_tab` button. fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle { Rectangle { x: bounds.x, y: bounds.y + button_height / 4.0, width: button_height, height: button_height, } } #[allow(clippy::too_many_arguments)] fn draw_icon( renderer: &mut Renderer, theme: &crate::Theme, style: &renderer::Style, cursor: mouse::Cursor, viewport: &Rectangle, color: Color, bounds: Rectangle, icon: Icon, ) { let layout_node = layout::Node::new(Size { width: bounds.width, height: bounds.width, }) .move_to(Point { x: bounds.x, y: bounds.y, }); Widget::::draw( Element::::from(icon.clone()).as_widget(), &Tree::empty(), renderer, theme, &renderer::Style { icon_color: color, text_color: color, scale_factor: style.scale_factor, }, Layout::new(&layout_node), cursor, viewport, ); }