2023-01-06 01:39:09 +01:00
|
|
|
// Copyright 2022 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
use super::model::{Entity, Model, Selectable};
|
2023-01-04 05:37:20 +01:00
|
|
|
use super::style::StyleSheet;
|
2023-02-13 16:09:05 +01:00
|
|
|
use super::IconColor;
|
2023-02-13 15:57:30 +01:00
|
|
|
use crate::widget::{icon, IconSource};
|
2023-01-04 05:37:20 +01:00
|
|
|
use derive_setters::Setters;
|
|
|
|
|
use iced::{
|
2023-01-09 16:18:02 +01:00
|
|
|
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
|
2023-08-15 17:20:19 +02:00
|
|
|
Rectangle, Size,
|
2023-01-04 05:37:20 +01:00
|
|
|
};
|
2023-05-30 12:03:15 -04:00
|
|
|
use iced_core::text::{LineHeight, Shaping};
|
|
|
|
|
use iced_core::widget::{self, operation, tree};
|
2023-06-15 11:16:32 -04:00
|
|
|
use iced_core::BorderRadius;
|
2023-05-30 12:03:15 -04:00
|
|
|
use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
2023-02-13 15:57:30 +01:00
|
|
|
use std::marker::PhantomData;
|
2023-01-04 05:37:20 +01:00
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
/// State that is maintained by each individual widget.
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct LocalState {
|
|
|
|
|
/// The first focusable key.
|
2023-01-17 18:49:40 +01:00
|
|
|
first: Entity,
|
2023-01-09 16:18:02 +01:00
|
|
|
/// If the widget is focused or not.
|
|
|
|
|
focused: bool,
|
|
|
|
|
/// The key inside the widget that is currently focused.
|
2023-01-17 18:49:40 +01:00
|
|
|
focused_key: Entity,
|
2023-01-09 16:18:02 +01:00
|
|
|
/// The ID of the button that is being hovered. Defaults to null.
|
2023-01-17 18:49:40 +01:00
|
|
|
hovered: Entity,
|
2023-01-09 16:18:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl operation::Focusable for LocalState {
|
|
|
|
|
fn is_focused(&self) -> bool {
|
|
|
|
|
self.focused
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn focus(&mut self) {
|
|
|
|
|
self.focused = true;
|
|
|
|
|
self.focused_key = self.first;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unfocus(&mut self) {
|
|
|
|
|
self.focused = false;
|
2023-01-17 18:49:40 +01:00
|
|
|
self.focused_key = Entity::default();
|
2023-01-09 16:18:02 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-04 05:37:20 +01:00
|
|
|
/// Isolates variant-specific behaviors from [`SegmentedButton`].
|
|
|
|
|
pub trait SegmentedVariant {
|
2023-05-30 12:03:15 -04:00
|
|
|
type Renderer: iced_core::Renderer;
|
2023-01-04 05:37:20 +01:00
|
|
|
|
|
|
|
|
/// Get the appearance for this variant of the widget.
|
|
|
|
|
fn variant_appearance(
|
2023-05-30 12:03:15 -04:00
|
|
|
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
|
|
|
|
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
2023-01-04 05:37:20 +01:00
|
|
|
) -> super::Appearance
|
|
|
|
|
where
|
2023-05-30 12:03:15 -04:00
|
|
|
<Self::Renderer as iced_core::Renderer>::Theme: StyleSheet;
|
2023-01-04 05:37:20 +01:00
|
|
|
|
|
|
|
|
/// Calculates the bounds for the given button by its position.
|
|
|
|
|
fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle;
|
|
|
|
|
|
|
|
|
|
/// Calculates the layout of this variant.
|
|
|
|
|
fn variant_layout(&self, renderer: &Self::Renderer, limits: &layout::Limits) -> layout::Node;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
/// A conjoined group of items that function together as a button.
|
2023-01-04 05:37:20 +01:00
|
|
|
#[derive(Setters)]
|
2023-01-17 18:49:40 +01:00
|
|
|
pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
2023-01-04 05:37:20 +01:00
|
|
|
where
|
2023-05-30 12:03:15 -04:00
|
|
|
Renderer: iced_core::Renderer
|
|
|
|
|
+ iced_core::text::Renderer
|
|
|
|
|
+ iced_core::image::Renderer
|
|
|
|
|
+ iced_core::svg::Renderer,
|
2023-01-04 05:37:20 +01:00
|
|
|
Renderer::Theme: StyleSheet,
|
2023-01-17 18:49:40 +01:00
|
|
|
Model<SelectionMode>: Selectable,
|
|
|
|
|
SelectionMode: Default,
|
2023-01-04 05:37:20 +01:00
|
|
|
{
|
2023-01-09 16:18:02 +01:00
|
|
|
/// The model borrowed from the application create this widget.
|
2023-01-04 05:37:20 +01:00
|
|
|
#[setters(skip)]
|
2023-01-17 18:49:40 +01:00
|
|
|
pub(super) model: &'a Model<SelectionMode>,
|
|
|
|
|
/// iced widget ID
|
2023-01-09 16:18:02 +01:00
|
|
|
pub(super) id: Option<Id>,
|
2023-02-13 15:57:30 +01:00
|
|
|
/// The icon used for the close button.
|
|
|
|
|
pub(super) close_icon: IconSource<'a>,
|
|
|
|
|
/// Show the close icon only when item is hovered.
|
|
|
|
|
pub(super) show_close_icon_on_hover: bool,
|
2023-01-06 01:39:09 +01:00
|
|
|
/// 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,
|
2023-01-04 05:37:20 +01:00
|
|
|
/// Desired font for active tabs.
|
2023-05-30 12:03:15 -04:00
|
|
|
pub(super) font_active: Option<Renderer::Font>,
|
2023-01-04 05:37:20 +01:00
|
|
|
/// Desired font for hovered tabs.
|
2023-05-30 12:03:15 -04:00
|
|
|
pub(super) font_hovered: Option<Renderer::Font>,
|
2023-01-04 05:37:20 +01:00
|
|
|
/// Desired font for inactive tabs.
|
2023-05-30 12:03:15 -04:00
|
|
|
pub(super) font_inactive: Option<Renderer::Font>,
|
2023-01-27 04:44:25 +01:00
|
|
|
/// Size of the font.
|
2023-05-30 12:03:15 -04:00
|
|
|
pub(super) font_size: f32,
|
2023-01-06 01:39:09 +01:00
|
|
|
/// Size of icon
|
|
|
|
|
pub(super) icon_size: u16,
|
2023-01-04 05:37:20 +01:00
|
|
|
/// Desired width of the widget.
|
|
|
|
|
pub(super) width: Length,
|
|
|
|
|
/// Desired height of the widget.
|
|
|
|
|
pub(super) height: Length,
|
2023-01-09 16:18:02 +01:00
|
|
|
/// Desired spacing between items.
|
2023-01-04 05:37:20 +01:00
|
|
|
pub(super) spacing: u16,
|
2023-05-30 12:03:15 -04:00
|
|
|
/// LineHeight of the font.
|
|
|
|
|
pub(super) line_height: LineHeight,
|
2023-01-04 05:37:20 +01:00
|
|
|
/// Style to draw the widget in.
|
|
|
|
|
#[setters(into)]
|
|
|
|
|
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
|
2023-02-13 15:57:30 +01:00
|
|
|
/// Emits the ID of the item that was activated.
|
|
|
|
|
#[setters(strip_option)]
|
|
|
|
|
pub(super) on_activate: Option<fn(Entity) -> Message>,
|
|
|
|
|
#[setters(strip_option)]
|
|
|
|
|
pub(super) on_close: Option<fn(Entity) -> Message>,
|
2023-01-04 05:37:20 +01:00
|
|
|
#[setters(skip)]
|
|
|
|
|
/// Defines the implementation of this struct
|
|
|
|
|
variant: PhantomData<Variant>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
impl<'a, Variant, SelectionMode, Message, Renderer>
|
|
|
|
|
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
2023-01-04 05:37:20 +01:00
|
|
|
where
|
2023-05-30 12:03:15 -04:00
|
|
|
Renderer: iced_core::Renderer
|
|
|
|
|
+ iced_core::text::Renderer
|
|
|
|
|
+ iced_core::image::Renderer
|
|
|
|
|
+ iced_core::svg::Renderer,
|
2023-01-04 05:37:20 +01:00
|
|
|
Renderer::Theme: StyleSheet,
|
|
|
|
|
Self: SegmentedVariant<Renderer = Renderer>,
|
2023-01-17 18:49:40 +01:00
|
|
|
Model<SelectionMode>: Selectable,
|
|
|
|
|
SelectionMode: Default,
|
2023-01-04 05:37:20 +01:00
|
|
|
{
|
|
|
|
|
#[must_use]
|
2023-01-17 18:49:40 +01:00
|
|
|
pub fn new(model: &'a Model<SelectionMode>) -> Self {
|
2023-01-04 05:37:20 +01:00
|
|
|
Self {
|
2023-01-17 18:49:40 +01:00
|
|
|
model,
|
2023-01-09 16:18:02 +01:00
|
|
|
id: None,
|
2023-02-13 15:57:30 +01:00
|
|
|
close_icon: IconSource::from("window-close-symbolic"),
|
|
|
|
|
show_close_icon_on_hover: false,
|
2023-01-06 01:39:09 +01:00
|
|
|
button_padding: [4, 4, 4, 4],
|
|
|
|
|
button_height: 32,
|
|
|
|
|
button_spacing: 4,
|
2023-05-30 12:03:15 -04:00
|
|
|
font_active: None,
|
|
|
|
|
font_hovered: None,
|
|
|
|
|
font_inactive: None,
|
2023-05-30 23:46:49 +02:00
|
|
|
font_size: 14.0,
|
2023-01-27 04:44:25 +01:00
|
|
|
icon_size: 16,
|
2023-01-04 05:37:20 +01:00
|
|
|
height: Length::Shrink,
|
|
|
|
|
width: Length::Fill,
|
|
|
|
|
spacing: 0,
|
2023-05-30 12:03:15 -04:00
|
|
|
line_height: LineHeight::default(),
|
2023-01-04 05:37:20 +01:00
|
|
|
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
|
|
|
|
on_activate: None,
|
2023-02-13 15:57:30 +01:00
|
|
|
on_close: None,
|
2023-01-04 05:37:20 +01:00
|
|
|
variant: PhantomData,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:18:54 +01:00
|
|
|
/// Check if an item is enabled.
|
2023-01-17 18:49:40 +01:00
|
|
|
fn is_enabled(&self, key: Entity) -> bool {
|
2023-01-09 18:18:54 +01:00
|
|
|
self.model.items.get(key).map_or(false, |item| item.enabled)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Focus the previous item in the widget.
|
2023-01-09 16:18:02 +01:00
|
|
|
fn focus_previous(&mut self, state: &mut LocalState) -> event::Status {
|
2023-01-09 19:26:31 +01:00
|
|
|
let mut keys = self.model.order.iter().copied().rev();
|
2023-01-09 16:18:02 +01:00
|
|
|
|
2023-01-09 19:26:31 +01:00
|
|
|
while let Some(key) = keys.next() {
|
2023-01-09 16:18:02 +01:00
|
|
|
if key == state.focused_key {
|
2023-01-09 19:26:31 +01:00
|
|
|
for key in keys {
|
|
|
|
|
// Skip disabled buttons.
|
|
|
|
|
if !self.is_enabled(key) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:18:54 +01:00
|
|
|
state.focused_key = key;
|
|
|
|
|
return event::Status::Captured;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
2023-01-09 16:18:02 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
state.focused_key = Entity::default();
|
2023-01-09 16:18:02 +01:00
|
|
|
event::Status::Ignored
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:18:54 +01:00
|
|
|
/// Focus the next item in the widget.
|
2023-01-09 16:18:02 +01:00
|
|
|
fn focus_next(&mut self, state: &mut LocalState) -> event::Status {
|
2023-01-09 19:26:31 +01:00
|
|
|
let mut keys = self.model.order.iter().copied();
|
2023-01-09 16:18:02 +01:00
|
|
|
|
|
|
|
|
while let Some(key) = keys.next() {
|
|
|
|
|
if key == state.focused_key {
|
2023-01-09 18:18:54 +01:00
|
|
|
for key in keys {
|
|
|
|
|
// Skip disabled buttons.
|
|
|
|
|
if !self.is_enabled(key) {
|
|
|
|
|
continue;
|
2023-01-09 16:18:02 +01:00
|
|
|
}
|
2023-01-09 18:18:54 +01:00
|
|
|
|
|
|
|
|
state.focused_key = key;
|
|
|
|
|
return event::Status::Captured;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
2023-01-09 16:18:02 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
state.focused_key = Entity::default();
|
2023-01-09 16:18:02 +01:00
|
|
|
event::Status::Ignored
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-27 04:44:25 +01:00
|
|
|
pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) {
|
2023-01-04 05:37:20 +01:00
|
|
|
let mut width = 0.0f32;
|
|
|
|
|
let mut height = 0.0f32;
|
2023-05-30 12:03:15 -04:00
|
|
|
let font = renderer.default_font();
|
2023-01-04 05:37:20 +01:00
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
for key in self.model.order.iter().copied() {
|
2023-01-06 01:39:09 +01:00
|
|
|
let mut button_width = 0.0f32;
|
|
|
|
|
let mut button_height = 0.0f32;
|
|
|
|
|
|
|
|
|
|
// Add text to measurement if text was given.
|
2023-01-17 18:49:40 +01:00
|
|
|
if let Some(text) = self.model.text(key) {
|
2023-08-21 11:52:19 -04:00
|
|
|
let Size { width, height } = renderer.measure(
|
2023-05-30 12:03:15 -04:00
|
|
|
text,
|
|
|
|
|
self.font_size,
|
|
|
|
|
self.line_height,
|
|
|
|
|
font,
|
|
|
|
|
bounds,
|
|
|
|
|
Shaping::Advanced,
|
|
|
|
|
);
|
2023-01-06 01:39:09 +01:00
|
|
|
|
2023-08-21 11:52:19 -04:00
|
|
|
button_width = width;
|
|
|
|
|
button_height = height;
|
2023-01-06 01:39:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add icon to measurement if icon was given.
|
2023-01-17 18:49:40 +01:00
|
|
|
if self.model.icon(key).is_some() {
|
2023-02-13 15:57:30 +01:00
|
|
|
button_height = button_height.max(f32::from(self.icon_size));
|
|
|
|
|
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add close button to measurement if found.
|
|
|
|
|
if self.model.is_closable(key) {
|
|
|
|
|
button_height = button_height.max(f32::from(self.icon_size));
|
2023-02-13 17:38:23 +01:00
|
|
|
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing) + 8.0;
|
2023-01-06 01:39:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
height = height.max(button_height);
|
|
|
|
|
width = width.max(button_width);
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-06 01:39:09 +01:00
|
|
|
// 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));
|
|
|
|
|
|
2023-01-04 05:37:20 +01:00
|
|
|
(width, height)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
impl<'a, Variant, SelectionMode, Message, Renderer> Widget<Message, Renderer>
|
|
|
|
|
for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
2023-01-04 05:37:20 +01:00
|
|
|
where
|
2023-05-30 12:03:15 -04:00
|
|
|
Renderer: iced_core::Renderer
|
|
|
|
|
+ iced_core::text::Renderer
|
|
|
|
|
+ iced_core::image::Renderer
|
|
|
|
|
+ iced_core::svg::Renderer,
|
2023-01-04 05:37:20 +01:00
|
|
|
Renderer::Theme: StyleSheet,
|
|
|
|
|
Self: SegmentedVariant<Renderer = Renderer>,
|
2023-01-17 18:49:40 +01:00
|
|
|
Model<SelectionMode>: Selectable,
|
|
|
|
|
SelectionMode: Default,
|
2023-01-04 05:37:20 +01:00
|
|
|
Message: 'static + Clone,
|
|
|
|
|
{
|
|
|
|
|
fn tag(&self) -> tree::Tag {
|
2023-01-09 16:18:02 +01:00
|
|
|
tree::Tag::of::<LocalState>()
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn state(&self) -> tree::State {
|
2023-01-09 16:18:02 +01:00
|
|
|
tree::State::new(LocalState {
|
2023-01-09 19:26:31 +01:00
|
|
|
first: self.model.order.iter().copied().next().unwrap_or_default(),
|
2023-01-09 16:18:02 +01:00
|
|
|
..LocalState::default()
|
|
|
|
|
})
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn width(&self) -> Length {
|
|
|
|
|
self.width
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn height(&self) -> Length {
|
|
|
|
|
self.height
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
|
|
|
|
self.variant_layout(renderer, limits)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_event(
|
|
|
|
|
&mut self,
|
|
|
|
|
tree: &mut Tree,
|
|
|
|
|
event: Event,
|
|
|
|
|
layout: Layout<'_>,
|
2023-06-15 11:16:32 -04:00
|
|
|
cursor_position: mouse::Cursor,
|
2023-01-04 05:37:20 +01:00
|
|
|
_renderer: &Renderer,
|
|
|
|
|
_clipboard: &mut dyn Clipboard,
|
|
|
|
|
shell: &mut Shell<'_, Message>,
|
2023-08-21 11:52:19 -04:00
|
|
|
_viewport: &iced::Rectangle,
|
2023-01-04 05:37:20 +01:00
|
|
|
) -> event::Status {
|
|
|
|
|
let bounds = layout.bounds();
|
2023-01-09 16:18:02 +01:00
|
|
|
let state = tree.state.downcast_mut::<LocalState>();
|
2023-01-04 05:37:20 +01:00
|
|
|
|
2023-06-15 11:16:32 -04:00
|
|
|
if cursor_position.is_over(bounds) {
|
2023-01-09 19:26:31 +01:00
|
|
|
for (nth, key) in self.model.order.iter().copied().enumerate() {
|
2023-01-04 05:37:20 +01:00
|
|
|
let bounds = self.variant_button_bounds(bounds, nth);
|
2023-06-15 11:16:32 -04:00
|
|
|
if cursor_position.is_over(bounds) {
|
2023-01-09 19:26:31 +01:00
|
|
|
if self.model.items[key].enabled {
|
2023-01-19 22:32:58 +01:00
|
|
|
// Record that the mouse is hovering over this button.
|
|
|
|
|
state.hovered = key;
|
|
|
|
|
|
2023-02-13 15:57:30 +01:00
|
|
|
// If marked as closable, show a close icon.
|
|
|
|
|
if self.model.items[key].closable {
|
|
|
|
|
if let Some(on_close) = self.on_close.as_ref() {
|
2023-06-15 11:16:32 -04:00
|
|
|
if cursor_position.is_over(close_bounds(
|
2023-02-13 15:57:30 +01:00
|
|
|
bounds,
|
|
|
|
|
f32::from(self.icon_size),
|
|
|
|
|
self.button_padding,
|
2023-06-15 11:16:32 -04:00
|
|
|
)) {
|
2023-02-13 15:57:30 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-01-09 16:18:02 +01:00
|
|
|
|
|
|
|
|
break;
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-01-17 18:49:40 +01:00
|
|
|
state.hovered = Entity::default();
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
if state.focused {
|
|
|
|
|
if let Event::Keyboard(keyboard::Event::KeyPressed {
|
|
|
|
|
key_code: keyboard::KeyCode::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_code: keyboard::KeyCode::Enter,
|
|
|
|
|
..
|
|
|
|
|
}) = event
|
|
|
|
|
{
|
|
|
|
|
shell.publish(on_activate(state.focused_key));
|
|
|
|
|
return event::Status::Captured;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-04 05:37:20 +01:00
|
|
|
event::Status::Ignored
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
fn operate(
|
|
|
|
|
&self,
|
|
|
|
|
tree: &mut Tree,
|
|
|
|
|
_layout: Layout<'_>,
|
2023-05-30 12:03:15 -04:00
|
|
|
_renderer: &Renderer,
|
|
|
|
|
operation: &mut dyn iced_core::widget::Operation<
|
|
|
|
|
iced_core::widget::OperationOutputWrapper<Message>,
|
|
|
|
|
>,
|
2023-01-09 16:18:02 +01:00
|
|
|
) {
|
|
|
|
|
let state = tree.state.downcast_mut::<LocalState>();
|
|
|
|
|
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-04 05:37:20 +01:00
|
|
|
fn mouse_interaction(
|
|
|
|
|
&self,
|
|
|
|
|
_tree: &Tree,
|
|
|
|
|
layout: Layout<'_>,
|
2023-06-15 11:16:32 -04:00
|
|
|
cursor_position: mouse::Cursor,
|
2023-01-04 05:37:20 +01:00
|
|
|
_viewport: &iced::Rectangle,
|
|
|
|
|
_renderer: &Renderer,
|
2023-05-30 12:03:15 -04:00
|
|
|
) -> iced_core::mouse::Interaction {
|
2023-01-04 05:37:20 +01:00
|
|
|
let bounds = layout.bounds();
|
2023-01-09 16:18:02 +01:00
|
|
|
|
2023-06-15 11:16:32 -04:00
|
|
|
if cursor_position.is_over(bounds) {
|
2023-01-09 19:26:31 +01:00
|
|
|
for (nth, key) in self.model.order.iter().copied().enumerate() {
|
2023-06-15 11:16:32 -04:00
|
|
|
if cursor_position.is_over(self.variant_button_bounds(bounds, nth)) {
|
2023-01-09 19:26:31 +01:00
|
|
|
return if self.model.items[key].enabled {
|
2023-05-30 12:03:15 -04:00
|
|
|
iced_core::mouse::Interaction::Pointer
|
2023-01-09 16:18:02 +01:00
|
|
|
} else {
|
2023-05-30 12:03:15 -04:00
|
|
|
iced_core::mouse::Interaction::Idle
|
2023-01-09 16:18:02 +01:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
2023-01-09 16:18:02 +01:00
|
|
|
|
2023-05-30 12:03:15 -04:00
|
|
|
iced_core::mouse::Interaction::Idle
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-06 01:39:09 +01:00
|
|
|
#[allow(clippy::too_many_lines)]
|
2023-01-04 05:37:20 +01:00
|
|
|
fn draw(
|
|
|
|
|
&self,
|
|
|
|
|
tree: &Tree,
|
|
|
|
|
renderer: &mut Renderer,
|
2023-05-30 12:03:15 -04:00
|
|
|
theme: &<Renderer as iced_core::Renderer>::Theme,
|
2023-01-04 05:37:20 +01:00
|
|
|
_style: &renderer::Style,
|
|
|
|
|
layout: Layout<'_>,
|
2023-06-15 11:16:32 -04:00
|
|
|
_cursor_position: mouse::Cursor,
|
2023-01-04 05:37:20 +01:00
|
|
|
_viewport: &iced::Rectangle,
|
|
|
|
|
) {
|
2023-01-09 16:18:02 +01:00
|
|
|
let state = tree.state.downcast_ref::<LocalState>();
|
2023-01-04 05:37:20 +01:00
|
|
|
let appearance = Self::variant_appearance(theme, &self.style);
|
|
|
|
|
let bounds = layout.bounds();
|
2023-01-09 16:18:02 +01:00
|
|
|
let button_amount = self.model.items.len();
|
2023-01-04 05:37:20 +01:00
|
|
|
|
|
|
|
|
// Draw the background, if a background was defined.
|
|
|
|
|
if let Some(background) = appearance.background {
|
|
|
|
|
renderer.fill_quad(
|
|
|
|
|
renderer::Quad {
|
|
|
|
|
bounds,
|
|
|
|
|
border_radius: appearance.border_radius,
|
|
|
|
|
border_width: 0.0,
|
|
|
|
|
border_color: Color::TRANSPARENT,
|
|
|
|
|
},
|
|
|
|
|
background,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
// Draw each of the items in the widget.
|
2023-01-09 19:26:31 +01:00
|
|
|
for (nth, key) in self.model.order.iter().copied().enumerate() {
|
2023-01-06 01:39:09 +01:00
|
|
|
let mut bounds = self.variant_button_bounds(bounds, nth);
|
2023-01-04 05:37:20 +01:00
|
|
|
|
2023-02-13 15:57:30 +01:00
|
|
|
let key_is_active = self.model.is_active(key);
|
|
|
|
|
let key_is_hovered = state.hovered == key;
|
|
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
let (status_appearance, font) = if state.focused_key == key {
|
|
|
|
|
(appearance.focus, &self.font_active)
|
2023-02-13 15:57:30 +01:00
|
|
|
} else if key_is_active {
|
2023-01-04 05:37:20 +01:00
|
|
|
(appearance.active, &self.font_active)
|
2023-02-13 15:57:30 +01:00
|
|
|
} else if key_is_hovered {
|
2023-01-04 05:37:20 +01:00
|
|
|
(appearance.hover, &self.font_hovered)
|
|
|
|
|
} else {
|
|
|
|
|
(appearance.inactive, &self.font_inactive)
|
|
|
|
|
};
|
2023-05-30 12:03:15 -04:00
|
|
|
let font = font.unwrap_or_else(|| renderer.default_font());
|
2023-01-04 05:37:20 +01:00
|
|
|
|
|
|
|
|
let button_appearance = if nth == 0 {
|
|
|
|
|
status_appearance.first
|
|
|
|
|
} else if nth + 1 == button_amount {
|
|
|
|
|
status_appearance.last
|
|
|
|
|
} else {
|
|
|
|
|
status_appearance.middle
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-13 16:09:05 +01:00
|
|
|
let icon_color = match self.model.data::<IconColor>(key).copied() {
|
|
|
|
|
Some(IconColor::None) => None,
|
|
|
|
|
Some(IconColor::Color(color)) => Some(color),
|
|
|
|
|
None => Some(status_appearance.text_color),
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-04 05:37:20 +01:00
|
|
|
// Render the background of the button.
|
|
|
|
|
if status_appearance.background.is_some() {
|
|
|
|
|
renderer.fill_quad(
|
|
|
|
|
renderer::Quad {
|
|
|
|
|
bounds,
|
|
|
|
|
border_radius: button_appearance.border_radius,
|
|
|
|
|
border_width: 0.0,
|
|
|
|
|
border_color: Color::TRANSPARENT,
|
|
|
|
|
},
|
|
|
|
|
status_appearance
|
|
|
|
|
.background
|
|
|
|
|
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
renderer.fill_quad(
|
|
|
|
|
renderer::Quad {
|
|
|
|
|
bounds,
|
|
|
|
|
border_radius: BorderRadius::from(0.0),
|
|
|
|
|
border_width: 0.0,
|
|
|
|
|
border_color: Color::TRANSPARENT,
|
|
|
|
|
},
|
|
|
|
|
background,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-13 15:57:30 +01:00
|
|
|
let original_bounds = bounds;
|
|
|
|
|
|
2023-01-06 01:39:09 +01:00
|
|
|
let y = bounds.center_y();
|
|
|
|
|
|
|
|
|
|
// Draw the image beside the text.
|
2023-01-17 18:49:40 +01:00
|
|
|
let horizontal_alignment = if let Some(icon) = self.model.icon(key) {
|
2023-01-06 01:39:09 +01:00
|
|
|
bounds.x += f32::from(self.button_padding[0]);
|
|
|
|
|
bounds.y += f32::from(self.button_padding[1]);
|
|
|
|
|
bounds.width -=
|
|
|
|
|
f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]);
|
|
|
|
|
bounds.height -=
|
|
|
|
|
f32::from(self.button_padding[1]) - f32::from(self.button_padding[3]);
|
|
|
|
|
|
|
|
|
|
let width = f32::from(self.icon_size);
|
|
|
|
|
let offset = width + f32::from(self.button_spacing);
|
|
|
|
|
bounds.y = y - width / 2.0;
|
|
|
|
|
|
|
|
|
|
let icon_bounds = Rectangle {
|
|
|
|
|
width,
|
|
|
|
|
height: width,
|
|
|
|
|
..bounds
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
bounds.x += offset;
|
|
|
|
|
bounds.width -= offset;
|
2023-02-13 15:57:30 +01:00
|
|
|
|
2023-07-07 16:39:22 -04:00
|
|
|
match icon.load(self.icon_size, None, false, true) {
|
2023-02-13 15:57:30 +01:00
|
|
|
icon::Handle::Image(_handle) => {
|
2023-01-06 01:39:09 +01:00
|
|
|
unimplemented!()
|
|
|
|
|
}
|
2023-02-13 15:57:30 +01:00
|
|
|
icon::Handle::Svg(handle) => {
|
2023-05-30 12:03:15 -04:00
|
|
|
iced_core::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds);
|
2023-01-06 01:39:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alignment::Horizontal::Left
|
|
|
|
|
} else {
|
|
|
|
|
bounds.x = bounds.center_x();
|
|
|
|
|
alignment::Horizontal::Center
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
if let Some(text) = self.model.text(key) {
|
2023-01-06 01:39:09 +01:00
|
|
|
bounds.y = y;
|
|
|
|
|
|
|
|
|
|
// Draw the text in this button.
|
2023-05-30 12:03:15 -04:00
|
|
|
renderer.fill_text(iced_core::text::Text {
|
2023-01-06 01:39:09 +01:00
|
|
|
content: text,
|
2023-05-30 12:03:15 -04:00
|
|
|
size: self.font_size,
|
2023-01-06 01:39:09 +01:00
|
|
|
bounds,
|
|
|
|
|
color: status_appearance.text_color,
|
2023-05-30 12:03:15 -04:00
|
|
|
font,
|
2023-01-06 01:39:09 +01:00
|
|
|
horizontal_alignment,
|
|
|
|
|
vertical_alignment: alignment::Vertical::Center,
|
2023-05-30 12:03:15 -04:00
|
|
|
shaping: Shaping::Advanced,
|
|
|
|
|
line_height: self.line_height,
|
2023-01-06 01:39:09 +01:00
|
|
|
});
|
|
|
|
|
}
|
2023-02-13 15:57:30 +01:00
|
|
|
|
|
|
|
|
let show_close_button =
|
|
|
|
|
(key_is_active || !self.show_close_icon_on_hover || key_is_hovered)
|
|
|
|
|
&& self.model.is_closable(key);
|
|
|
|
|
|
|
|
|
|
// Draw a close button if this is set.
|
|
|
|
|
if show_close_button {
|
|
|
|
|
let width = f32::from(self.icon_size);
|
|
|
|
|
let icon_bounds = close_bounds(original_bounds, width, self.button_padding);
|
|
|
|
|
|
2023-07-07 16:39:22 -04:00
|
|
|
match self.close_icon.load(self.icon_size, None, false, true) {
|
2023-02-13 15:57:30 +01:00
|
|
|
icon::Handle::Image(_handle) => {
|
|
|
|
|
unimplemented!()
|
|
|
|
|
}
|
|
|
|
|
icon::Handle::Svg(handle) => {
|
2023-05-30 12:03:15 -04:00
|
|
|
iced_core::svg::Renderer::draw(
|
2023-02-13 15:57:30 +01:00
|
|
|
renderer,
|
|
|
|
|
handle,
|
|
|
|
|
Some(status_appearance.text_color),
|
|
|
|
|
icon_bounds,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn overlay<'b>(
|
2023-05-30 12:03:15 -04:00
|
|
|
&'b mut self,
|
2023-01-04 05:37:20 +01:00
|
|
|
_tree: &'b mut Tree,
|
2023-05-30 12:03:15 -04:00
|
|
|
_layout: iced_core::Layout<'_>,
|
2023-01-04 05:37:20 +01:00
|
|
|
_renderer: &Renderer,
|
2023-05-30 12:03:15 -04:00
|
|
|
) -> Option<iced_core::overlay::Element<'b, Message, Renderer>> {
|
2023-01-04 05:37:20 +01:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
impl<'a, Variant, SelectionMode, Message, Renderer>
|
|
|
|
|
From<SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>>
|
2023-01-04 05:37:20 +01:00
|
|
|
for Element<'a, Message, Renderer>
|
|
|
|
|
where
|
2023-05-30 12:03:15 -04:00
|
|
|
Renderer: iced_core::Renderer
|
|
|
|
|
+ iced_core::text::Renderer
|
|
|
|
|
+ iced_core::image::Renderer
|
|
|
|
|
+ iced_core::svg::Renderer
|
2023-01-06 01:39:09 +01:00
|
|
|
+ 'a,
|
2023-01-04 05:37:20 +01:00
|
|
|
Renderer::Theme: StyleSheet,
|
2023-01-17 18:49:40 +01:00
|
|
|
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>:
|
2023-01-06 16:18:25 +01:00
|
|
|
SegmentedVariant<Renderer = Renderer>,
|
2023-01-04 05:37:20 +01:00
|
|
|
Variant: 'static,
|
2023-01-17 18:49:40 +01:00
|
|
|
Model<SelectionMode>: Selectable,
|
|
|
|
|
SelectionMode: Default,
|
2023-01-04 05:37:20 +01:00
|
|
|
Message: 'static + Clone,
|
|
|
|
|
{
|
2023-01-17 18:49:40 +01:00
|
|
|
fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>) -> Self {
|
2023-01-09 16:18:02 +01:00
|
|
|
if widget.model.items.is_empty() {
|
2023-01-04 05:37:20 +01:00
|
|
|
widget.spacing = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Self::new(widget)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 16:18:02 +01:00
|
|
|
/// A command that focuses a segmented item stored in a widget.
|
|
|
|
|
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
|
|
|
|
|
Command::widget(operation::focusable::focus(id.0))
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 18:49:40 +01:00
|
|
|
/// The iced identifier of a segmented button.
|
2023-01-09 16:18:02 +01:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
|
|
|
pub struct Id(widget::Id);
|
|
|
|
|
|
|
|
|
|
impl Id {
|
|
|
|
|
/// Creates a custom [`Id`].
|
|
|
|
|
pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> 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<Id> for widget::Id {
|
|
|
|
|
fn from(id: Id) -> Self {
|
|
|
|
|
id.0
|
|
|
|
|
}
|
2023-01-04 05:37:20 +01:00
|
|
|
}
|
2023-02-13 15:57:30 +01:00
|
|
|
|
|
|
|
|
/// Calculates the bounds of the close button within the area of an item.
|
|
|
|
|
fn close_bounds(area: Rectangle<f32>, icon_size: f32, button_padding: [u16; 4]) -> Rectangle<f32> {
|
2023-02-13 17:38:23 +01:00
|
|
|
let unpadded_height = area.height - f32::from(button_padding[1]) - f32::from(button_padding[3]);
|
2023-02-13 15:57:30 +01:00
|
|
|
|
|
|
|
|
Rectangle {
|
2023-02-13 17:38:23 +01:00
|
|
|
x: area.x + area.width - icon_size - 8.0,
|
|
|
|
|
y: area.y + (unpadded_height / 2.0) - (icon_size / 2.0),
|
2023-02-13 15:57:30 +01:00
|
|
|
width: icon_size,
|
|
|
|
|
height: icon_size,
|
|
|
|
|
}
|
|
|
|
|
}
|