refactor!: separate horizontal and vertical segmented button widgets
- Removes the orientation enum in favor of two separate widgets - Implements the spacing attribute for both widgets - Demo is updated to display spaced variants of the widgets
This commit is contained in:
parent
3af1da6332
commit
444e389496
12 changed files with 969 additions and 547 deletions
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
name = "cosmic"
|
name = "cosmic"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["wayland"]
|
default = ["swbuf", "winit"]
|
||||||
debug = ["iced/debug"]
|
debug = ["iced/debug"]
|
||||||
swbuf = ["iced/swbuf", "iced_swbuf"]
|
swbuf = ["iced/swbuf", "iced_swbuf"]
|
||||||
wayland = ["iced/wayland", "iced_glow"]
|
wayland = ["iced/wayland", "iced_glow"]
|
||||||
|
|
@ -25,7 +25,6 @@ palette = "0.6.1"
|
||||||
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true }
|
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true }
|
||||||
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "3776d4a" }
|
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "3776d4a" }
|
||||||
slotmap = "1.0.6"
|
slotmap = "1.0.6"
|
||||||
stack_dst = "0.7.2"
|
|
||||||
|
|
||||||
[dependencies.cosmic-theme]
|
[dependencies.cosmic-theme]
|
||||||
git = "https://github.com/pop-os/cosmic-theme.git"
|
git = "https://github.com/pop-os/cosmic-theme.git"
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ use cosmic::{
|
||||||
iced_native::window,
|
iced_native::window,
|
||||||
iced_winit::window::{close, drag, minimize, toggle_maximize},
|
iced_winit::window::{close, drag, minimize, toggle_maximize},
|
||||||
theme::{self, Theme},
|
theme::{self, Theme},
|
||||||
widget::{
|
widget::{header_bar, icon, list, nav_bar, nav_button, scrollable, settings},
|
||||||
header_bar, icon, list, nav_bar, nav_button, scrollable, segmented_button, settings,
|
|
||||||
spin_button::{SpinButtonModel, SpinMessage},
|
|
||||||
},
|
|
||||||
Element, ElementExt,
|
Element, ElementExt,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ use cosmic::{
|
||||||
iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider},
|
iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider},
|
||||||
iced::{Alignment, Length},
|
iced::{Alignment, Length},
|
||||||
theme::{Button as ButtonTheme, Theme},
|
theme::{Button as ButtonTheme, Theme},
|
||||||
widget::{button, segmented_button::{self, cosmic::{view_switcher, segmented_selection}}, settings, toggler, Orientation, spin_button::{SpinButtonModel, SpinMessage}},
|
widget::{button, settings, toggler, spin_button::{SpinButtonModel, SpinMessage}},
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use cosmic::widget::segmented_button::{self, cosmic::{horizontal_segmented_selection, horizontal_view_switcher, vertical_segmented_selection, vertical_view_switcher}};
|
||||||
use super::{Page, Window};
|
use super::{Page, Window};
|
||||||
|
|
||||||
pub enum DemoView {
|
pub enum DemoView {
|
||||||
|
|
@ -79,7 +80,7 @@ impl State {
|
||||||
|
|
||||||
settings::view_column(vec![
|
settings::view_column(vec![
|
||||||
window.page_title(Page::Demo),
|
window.page_title(Page::Demo),
|
||||||
view_switcher(&self.view_switcher)
|
horizontal_view_switcher(&self.view_switcher)
|
||||||
.on_activate(Message::ViewSwitcher)
|
.on_activate(Message::ViewSwitcher)
|
||||||
.into(),
|
.into(),
|
||||||
match self.view_switcher.active_data() {
|
match self.view_switcher.active_data() {
|
||||||
|
|
@ -170,50 +171,71 @@ impl State {
|
||||||
cosmic::iced::widget::text("Selection")
|
cosmic::iced::widget::text("Selection")
|
||||||
.font(cosmic::font::FONT_SEMIBOLD)
|
.font(cosmic::font::FONT_SEMIBOLD)
|
||||||
.into(),
|
.into(),
|
||||||
segmented_selection(&self.selection)
|
cosmic::iced::widget::text("Horizontal").into(),
|
||||||
|
horizontal_segmented_selection(&self.selection)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.into(),
|
.into(),
|
||||||
segmented_selection(&self.selection)
|
cosmic::iced::widget::text("Horizontal With Spacing").into(),
|
||||||
|
horizontal_segmented_selection(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.into(),
|
.into(),
|
||||||
|
cosmic::iced::widget::text("Vertical").into(),
|
||||||
|
vertical_segmented_selection(&self.selection)
|
||||||
|
.on_activate(Message::Selection)
|
||||||
|
.into(),
|
||||||
|
cosmic::iced::widget::text("Vertical With Spacing").into(),
|
||||||
cosmic::iced::widget::row(vec![
|
cosmic::iced::widget::row(vec![
|
||||||
segmented_selection(&self.selection)
|
vertical_segmented_selection(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.into(),
|
.into(),
|
||||||
segmented_selection(&self.selection)
|
vertical_segmented_selection(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.into(),
|
.into(),
|
||||||
segmented_selection(&self.selection)
|
vertical_segmented_selection(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.into(),
|
.into(),
|
||||||
])
|
])
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into(),
|
.into(),
|
||||||
cosmic::iced::widget::text("ViewSwitcher")
|
cosmic::iced::widget::text("View Switcher")
|
||||||
.font(cosmic::font::FONT_SEMIBOLD)
|
.font(cosmic::font::FONT_SEMIBOLD)
|
||||||
.into(),
|
.into(),
|
||||||
|
cosmic::iced::widget::text("Horizontal").into(),
|
||||||
|
horizontal_view_switcher(&self.selection)
|
||||||
|
.on_activate(Message::Selection)
|
||||||
|
.into(),
|
||||||
|
cosmic::iced::widget::text("Horizontal With Spacing").into(),
|
||||||
|
horizontal_view_switcher(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
|
.on_activate(Message::Selection)
|
||||||
|
.into(),
|
||||||
|
cosmic::iced::widget::text("Vertical").into(),
|
||||||
|
vertical_view_switcher(&self.selection)
|
||||||
|
.on_activate(Message::Selection)
|
||||||
|
.into(),
|
||||||
|
cosmic::iced::widget::text("Vertical With Spacing").into(),
|
||||||
cosmic::iced::widget::row(vec![
|
cosmic::iced::widget::row(vec![
|
||||||
view_switcher(&self.selection)
|
vertical_view_switcher(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.into(),
|
.into(),
|
||||||
view_switcher(&self.selection)
|
vertical_view_switcher(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.into(),
|
.into(),
|
||||||
view_switcher(&self.selection)
|
vertical_view_switcher(&self.selection)
|
||||||
|
.spacing(8)
|
||||||
.on_activate(Message::Selection)
|
.on_activate(Message::Selection)
|
||||||
.orientation(Orientation::Vertical)
|
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.into(),
|
.into(),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
124
src/theme/mod.rs
124
src/theme/mod.rs
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
pub mod expander;
|
pub mod expander;
|
||||||
pub mod palette;
|
pub mod palette;
|
||||||
|
mod segmented_button;
|
||||||
|
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
|
|
||||||
pub use self::palette::Palette;
|
pub use self::palette::Palette;
|
||||||
|
pub use self::segmented_button::SegmentedButton;
|
||||||
|
|
||||||
use cosmic_theme::Component;
|
use cosmic_theme::Component;
|
||||||
use iced_core::BorderRadius;
|
use iced_core::BorderRadius;
|
||||||
|
|
@ -27,8 +29,6 @@ use iced_style::svg;
|
||||||
use iced_style::text;
|
use iced_style::text;
|
||||||
use iced_style::text_input;
|
use iced_style::text_input;
|
||||||
use iced_style::toggler;
|
use iced_style::toggler;
|
||||||
use crate::widget::Orientation;
|
|
||||||
use crate::widget::segmented_button;
|
|
||||||
|
|
||||||
use iced_core::{Background, Color};
|
use iced_core::{Background, Color};
|
||||||
|
|
||||||
|
|
@ -866,123 +866,3 @@ impl text_input::StyleSheet for Theme {
|
||||||
palette.primary.weak.color
|
palette.primary.weak.color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
|
||||||
pub enum SegmentedButton {
|
|
||||||
/// A tabbed widget for switching between views in an interface.
|
|
||||||
#[default]
|
|
||||||
ViewSwitcher,
|
|
||||||
/// A widget for multiple choice selection.
|
|
||||||
Selection,
|
|
||||||
/// Or implement any custom theme of your liking.
|
|
||||||
Custom(fn(&Theme) -> segmented_button::Appearance)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl segmented_button::StyleSheet for Theme {
|
|
||||||
type Style = SegmentedButton;
|
|
||||||
|
|
||||||
fn appearance(&self, style: &Self::Style, orientation: Orientation) -> segmented_button::Appearance {
|
|
||||||
match style {
|
|
||||||
SegmentedButton::ViewSwitcher => {
|
|
||||||
let cosmic = self.cosmic();
|
|
||||||
segmented_button::Appearance {
|
|
||||||
background: None,
|
|
||||||
border_color: Color::TRANSPARENT,
|
|
||||||
border_radius: BorderRadius::from(0.0),
|
|
||||||
border_width: 0.0,
|
|
||||||
button_active: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.primary.component.base.into())),
|
|
||||||
border_bottom: Some((4.0, cosmic.accent.base.into())),
|
|
||||||
border_radius_first: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
|
||||||
border_radius_last: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
|
||||||
border_radius_middle: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
|
||||||
text_color: cosmic.accent.base.into(),
|
|
||||||
},
|
|
||||||
button_inactive: segmented_button::ButtonAppearance {
|
|
||||||
background: None,
|
|
||||||
border_bottom: Some((1.0, cosmic.accent.base.into())),
|
|
||||||
border_radius_first: BorderRadius::from(0.0),
|
|
||||||
border_radius_last: BorderRadius::from(0.0),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.primary.on.into(),
|
|
||||||
},
|
|
||||||
button_hover: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
|
||||||
border_bottom: Some((1.0, cosmic.accent.base.into())),
|
|
||||||
border_radius_first: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
|
||||||
border_radius_last: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
|
||||||
border_radius_middle: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
|
||||||
text_color: cosmic.accent.base.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SegmentedButton::Selection if orientation == Orientation::Horizontal => {
|
|
||||||
let cosmic = self.cosmic();
|
|
||||||
segmented_button::Appearance {
|
|
||||||
background: None,
|
|
||||||
border_color: Color::TRANSPARENT,
|
|
||||||
border_radius: BorderRadius::from(0.0),
|
|
||||||
border_width: 0.0,
|
|
||||||
button_active: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.secondary.component.divider.into())),
|
|
||||||
border_bottom: None,
|
|
||||||
border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]),
|
|
||||||
border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.accent.base.into(),
|
|
||||||
},
|
|
||||||
button_inactive: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.secondary.component.base.into())),
|
|
||||||
border_bottom: None,
|
|
||||||
border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]),
|
|
||||||
border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.primary.on.into(),
|
|
||||||
},
|
|
||||||
button_hover: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
|
||||||
border_bottom: None,
|
|
||||||
border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]),
|
|
||||||
border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.accent.base.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SegmentedButton::Selection => {
|
|
||||||
let cosmic = self.cosmic();
|
|
||||||
segmented_button::Appearance {
|
|
||||||
background: None,
|
|
||||||
border_color: Color::TRANSPARENT,
|
|
||||||
border_radius: BorderRadius::from(0.0),
|
|
||||||
border_width: 0.0,
|
|
||||||
button_active: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.secondary.component.divider.into())),
|
|
||||||
border_bottom: None,
|
|
||||||
border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]),
|
|
||||||
border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.accent.base.into(),
|
|
||||||
},
|
|
||||||
button_inactive: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.secondary.component.base.into())),
|
|
||||||
border_bottom: None,
|
|
||||||
border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]),
|
|
||||||
border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.primary.on.into(),
|
|
||||||
},
|
|
||||||
button_hover: segmented_button::ButtonAppearance {
|
|
||||||
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
|
||||||
border_bottom: None,
|
|
||||||
border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]),
|
|
||||||
border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]),
|
|
||||||
border_radius_middle: BorderRadius::from(0.0),
|
|
||||||
text_color: cosmic.accent.base.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SegmentedButton::Custom(func) => func(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
src/theme/segmented_button.rs
Normal file
169
src/theme/segmented_button.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use crate::theme::Theme;
|
||||||
|
use crate::widget::segmented_button;
|
||||||
|
use iced_core::{Background, BorderRadius, Color};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
pub enum SegmentedButton {
|
||||||
|
/// A tabbed widget for switching between views in an interface.
|
||||||
|
#[default]
|
||||||
|
ViewSwitcher,
|
||||||
|
/// A widget for multiple choice selection.
|
||||||
|
Selection,
|
||||||
|
/// Or implement any custom theme of your liking.
|
||||||
|
Custom(fn(&Theme) -> segmented_button::Appearance),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl segmented_button::StyleSheet for Theme {
|
||||||
|
type Style = SegmentedButton;
|
||||||
|
|
||||||
|
fn horizontal(&self, style: &Self::Style) -> segmented_button::Appearance {
|
||||||
|
match style {
|
||||||
|
SegmentedButton::ViewSwitcher => {
|
||||||
|
let cosmic = self.cosmic();
|
||||||
|
segmented_button::Appearance {
|
||||||
|
background: None,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
border_radius: BorderRadius::from(0.0),
|
||||||
|
border_width: 0.0,
|
||||||
|
button_active: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.primary.component.base.into())),
|
||||||
|
border_bottom: Some((4.0, cosmic.accent.base.into())),
|
||||||
|
border_radius_first: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
||||||
|
border_radius_last: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
||||||
|
border_radius_middle: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
button_inactive: segmented_button::ButtonAppearance {
|
||||||
|
background: None,
|
||||||
|
border_bottom: Some((1.0, cosmic.accent.base.into())),
|
||||||
|
border_radius_first: BorderRadius::from(0.0),
|
||||||
|
border_radius_last: BorderRadius::from(0.0),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.primary.on.into(),
|
||||||
|
},
|
||||||
|
button_hover: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
||||||
|
border_bottom: Some((1.0, cosmic.accent.base.into())),
|
||||||
|
border_radius_first: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
||||||
|
border_radius_last: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
||||||
|
border_radius_middle: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SegmentedButton::Selection => {
|
||||||
|
let cosmic = self.cosmic();
|
||||||
|
segmented_button::Appearance {
|
||||||
|
background: None,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
border_radius: BorderRadius::from(0.0),
|
||||||
|
border_width: 0.0,
|
||||||
|
button_active: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
cosmic.secondary.component.divider.into(),
|
||||||
|
)),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]),
|
||||||
|
border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
button_inactive: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.secondary.component.base.into())),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]),
|
||||||
|
border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.primary.on.into(),
|
||||||
|
},
|
||||||
|
button_hover: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from([24.0, 0.0, 0.0, 24.0]),
|
||||||
|
border_radius_last: BorderRadius::from([0.0, 24.0, 24.0, 0.0]),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SegmentedButton::Custom(func) => func(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertical(&self, style: &Self::Style) -> segmented_button::Appearance {
|
||||||
|
match style {
|
||||||
|
SegmentedButton::ViewSwitcher => {
|
||||||
|
let cosmic = self.cosmic();
|
||||||
|
segmented_button::Appearance {
|
||||||
|
background: None,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
border_radius: BorderRadius::from(0.0),
|
||||||
|
border_width: 0.0,
|
||||||
|
button_active: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.primary.component.base.into())),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from(24.0),
|
||||||
|
border_radius_last: BorderRadius::from(24.0),
|
||||||
|
border_radius_middle: BorderRadius::from(24.0),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
button_inactive: segmented_button::ButtonAppearance {
|
||||||
|
background: None,
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from(24.0),
|
||||||
|
border_radius_last: BorderRadius::from(24.0),
|
||||||
|
border_radius_middle: BorderRadius::from(24.0),
|
||||||
|
text_color: cosmic.primary.on.into(),
|
||||||
|
},
|
||||||
|
button_hover: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from(24.0),
|
||||||
|
border_radius_last: BorderRadius::from(24.0),
|
||||||
|
border_radius_middle: BorderRadius::from(24.0),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SegmentedButton::Selection => {
|
||||||
|
let cosmic = self.cosmic();
|
||||||
|
segmented_button::Appearance {
|
||||||
|
background: None,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
border_radius: BorderRadius::from(0.0),
|
||||||
|
border_width: 0.0,
|
||||||
|
button_active: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
cosmic.secondary.component.divider.into(),
|
||||||
|
)),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]),
|
||||||
|
border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
button_inactive: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.secondary.component.base.into())),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]),
|
||||||
|
border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.primary.on.into(),
|
||||||
|
},
|
||||||
|
button_hover: segmented_button::ButtonAppearance {
|
||||||
|
background: Some(Background::Color(cosmic.primary.component.hover.into())),
|
||||||
|
border_bottom: None,
|
||||||
|
border_radius_first: BorderRadius::from([24.0, 24.0, 0.0, 0.0]),
|
||||||
|
border_radius_last: BorderRadius::from([0.0, 0.0, 24.0, 24.0]),
|
||||||
|
border_radius_middle: BorderRadius::from(0.0),
|
||||||
|
text_color: cosmic.accent.base.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SegmentedButton::Custom(func) => func(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,12 @@ mod toggler;
|
||||||
pub use toggler::toggler;
|
pub use toggler::toggler;
|
||||||
|
|
||||||
pub mod segmented_button;
|
pub mod segmented_button;
|
||||||
pub use segmented_button::{SegmentedButton, segmented_button};
|
pub use segmented_button::{
|
||||||
|
HorizontalSegmentedButton,
|
||||||
|
VerticalSegmentedButton,
|
||||||
|
horizontal_segmented_button,
|
||||||
|
vertical_segmented_button
|
||||||
|
};
|
||||||
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
|
|
@ -39,10 +44,3 @@ pub use spin_button::{SpinButton, spin_button};
|
||||||
pub mod rectangle_tracker;
|
pub mod rectangle_tracker;
|
||||||
|
|
||||||
pub mod aspect_ratio;
|
pub mod aspect_ratio;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
|
||||||
pub enum Orientation {
|
|
||||||
#[default]
|
|
||||||
Horizontal,
|
|
||||||
Vertical
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use super::{SegmentedButton, State};
|
use super::{HorizontalSegmentedButton, State, VerticalSegmentedButton};
|
||||||
|
|
||||||
/// Appears as a collection of tabs for developing a tabbed interface.
|
/// Appears as a collection of tabs for developing a tabbed interface.
|
||||||
///
|
///
|
||||||
/// The data for the widget comes from a [`State`] that is maintained the application.
|
/// The data for the widget comes from a [`State`] that is maintained the application.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn view_switcher<Message, Data>(
|
pub fn horizontal_view_switcher<Message, Data>(
|
||||||
state: &State<Data>,
|
state: &State<Data>,
|
||||||
) -> SegmentedButton<Message, crate::Renderer> {
|
) -> HorizontalSegmentedButton<Message, crate::Renderer> {
|
||||||
SegmentedButton::new(&state.inner)
|
HorizontalSegmentedButton::new(&state.inner)
|
||||||
.button_padding([16, 0, 16, 0])
|
.button_padding([16, 0, 16, 0])
|
||||||
.button_height(48)
|
.button_height(48)
|
||||||
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
||||||
|
|
@ -21,12 +21,40 @@ pub fn view_switcher<Message, Data>(
|
||||||
///
|
///
|
||||||
/// The data for the widget comes from a [`State`] that is maintained the application.
|
/// The data for the widget comes from a [`State`] that is maintained the application.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn segmented_selection<Message, Data>(
|
pub fn horizontal_segmented_selection<Message, Data>(
|
||||||
state: &State<Data>,
|
state: &State<Data>,
|
||||||
) -> SegmentedButton<Message, crate::Renderer> {
|
) -> HorizontalSegmentedButton<Message, crate::Renderer> {
|
||||||
SegmentedButton::new(&state.inner)
|
HorizontalSegmentedButton::new(&state.inner)
|
||||||
.button_padding([16, 0, 16, 0])
|
.button_padding([16, 0, 16, 0])
|
||||||
.button_height(32)
|
.button_height(32)
|
||||||
.style(crate::theme::SegmentedButton::Selection)
|
.style(crate::theme::SegmentedButton::Selection)
|
||||||
.font_active(crate::font::FONT_SEMIBOLD)
|
.font_active(crate::font::FONT_SEMIBOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Appears as a selection of choices for choosing between.
|
||||||
|
///
|
||||||
|
/// The data for the widget comes from a [`State`] that is maintained the application.
|
||||||
|
#[must_use]
|
||||||
|
pub fn vertical_segmented_selection<Message, Data>(
|
||||||
|
state: &State<Data>,
|
||||||
|
) -> VerticalSegmentedButton<Message, crate::Renderer> {
|
||||||
|
VerticalSegmentedButton::new(&state.inner)
|
||||||
|
.button_padding([16, 0, 16, 0])
|
||||||
|
.button_height(32)
|
||||||
|
.style(crate::theme::SegmentedButton::Selection)
|
||||||
|
.font_active(crate::font::FONT_SEMIBOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appears as a collection of tabs for developing a tabbed interface.
|
||||||
|
///
|
||||||
|
/// The data for the widget comes from a [`State`] that is maintained the application.
|
||||||
|
#[must_use]
|
||||||
|
pub fn vertical_view_switcher<Message, Data>(
|
||||||
|
state: &State<Data>,
|
||||||
|
) -> VerticalSegmentedButton<Message, crate::Renderer> {
|
||||||
|
VerticalSegmentedButton::new(&state.inner)
|
||||||
|
.button_padding([16, 0, 16, 0])
|
||||||
|
.button_height(48)
|
||||||
|
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
||||||
|
.font_active(crate::font::FONT_SEMIBOLD)
|
||||||
|
}
|
||||||
|
|
|
||||||
350
src/widget/segmented_button/horizontal.rs
Normal file
350
src/widget/segmented_button/horizontal.rs
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use super::state::{Key, SharedWidgetState, State};
|
||||||
|
use super::style::StyleSheet;
|
||||||
|
use super::UniqueWidgetState;
|
||||||
|
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use iced::{
|
||||||
|
alignment::{Horizontal, Vertical},
|
||||||
|
event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, Size,
|
||||||
|
};
|
||||||
|
use iced_core::BorderRadius;
|
||||||
|
use iced_native::widget::tree;
|
||||||
|
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
||||||
|
|
||||||
|
/// Creates a [`HorizontalSegmentedButton`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn horizontal_segmented_button<Message, Renderer, Data>(
|
||||||
|
state: &State<Data>,
|
||||||
|
) -> HorizontalSegmentedButton<Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
HorizontalSegmentedButton::new(&state.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget providing a conjoined set of horizontally-arranged buttons for choosing between.
|
||||||
|
///
|
||||||
|
/// The data for the widget comes from a [`State`] that is maintained the application.
|
||||||
|
#[derive(Setters)]
|
||||||
|
pub struct HorizontalSegmentedButton<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
/// Contains application state also used for drawing.
|
||||||
|
#[setters(skip)]
|
||||||
|
state: &'a SharedWidgetState,
|
||||||
|
/// Desired font for active tabs.
|
||||||
|
font_active: Renderer::Font,
|
||||||
|
/// Desired font for hovered tabs.
|
||||||
|
font_hovered: Renderer::Font,
|
||||||
|
/// Desired font for inactive tabs.
|
||||||
|
font_inactive: Renderer::Font,
|
||||||
|
/// Desired width of the widget.
|
||||||
|
width: Length,
|
||||||
|
/// Desired height of the widget.
|
||||||
|
height: Length,
|
||||||
|
/// Padding around a button.
|
||||||
|
button_padding: [u16; 4],
|
||||||
|
/// Desired height of a button.
|
||||||
|
button_height: u16,
|
||||||
|
/// Desired spacing between buttons.
|
||||||
|
spacing: u16,
|
||||||
|
/// Style to draw the widget in.
|
||||||
|
#[setters(into)]
|
||||||
|
style: <Renderer::Theme as StyleSheet>::Style,
|
||||||
|
/// Emits the ID of the activated widget on selection.
|
||||||
|
#[setters(skip)]
|
||||||
|
on_activate: Option<Box<dyn Fn(Key) -> Message>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> HorizontalSegmentedButton<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(state: &'a SharedWidgetState) -> Self {
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
font_active: Renderer::Font::default(),
|
||||||
|
font_hovered: Renderer::Font::default(),
|
||||||
|
font_inactive: Renderer::Font::default(),
|
||||||
|
height: Length::Shrink,
|
||||||
|
width: Length::Fill,
|
||||||
|
button_padding: [4, 4, 4, 4],
|
||||||
|
button_height: 32,
|
||||||
|
spacing: 0,
|
||||||
|
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||||
|
on_activate: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits the ID of the activated widget on selection.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self {
|
||||||
|
self.on_activate = Some(Box::from(on_activate));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a closure for generating the layout bounds of the buttons.
|
||||||
|
fn button_bounds(&self, bounds: Rectangle) -> impl FnMut() -> Rectangle {
|
||||||
|
let button_amount = self.state.buttons.len();
|
||||||
|
let width = bounds.width / button_amount as f32;
|
||||||
|
let spacing = self.spacing as f32;
|
||||||
|
let half = spacing / 2.0;
|
||||||
|
let mut bounds = bounds;
|
||||||
|
bounds.width = width;
|
||||||
|
let mut counter = 1;
|
||||||
|
|
||||||
|
move || {
|
||||||
|
let mut clone = bounds;
|
||||||
|
if counter == 1 {
|
||||||
|
clone.width -= half;
|
||||||
|
} else if counter == button_amount {
|
||||||
|
clone.x += spacing;
|
||||||
|
clone.width -= spacing;
|
||||||
|
} else {
|
||||||
|
clone.x += half;
|
||||||
|
clone.width -= half;
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds.x += width;
|
||||||
|
counter += 1;
|
||||||
|
clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_button(
|
||||||
|
&self,
|
||||||
|
renderer: &Renderer,
|
||||||
|
text: &str,
|
||||||
|
text_size: u16,
|
||||||
|
bounds: Size,
|
||||||
|
) -> (f32, f32) {
|
||||||
|
let (mut w, mut h) = renderer.measure(text, text_size, Default::default(), bounds);
|
||||||
|
w += self.button_padding[0] as f32 + self.button_padding[2] as f32;
|
||||||
|
h += self.button_padding[1] as f32 + self.button_padding[3] as f32;
|
||||||
|
h = h.max(self.button_height as f32);
|
||||||
|
(w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||||
|
for HorizontalSegmentedButton<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
Message: 'static + Clone,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<UniqueWidgetState>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(UniqueWidgetState::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(&self) -> Length {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> Length {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||||
|
let mut width = 0.0f32;
|
||||||
|
let mut height = 0.0f32;
|
||||||
|
let limits = limits.width(self.width);
|
||||||
|
let text_size = renderer.default_size();
|
||||||
|
|
||||||
|
for (_, content) in self.state.buttons.iter() {
|
||||||
|
let (w, h) = self.measure_button(renderer, &content.text, text_size, limits.max());
|
||||||
|
width += w + f32::from(self.spacing * 2);
|
||||||
|
height = height.max(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = limits
|
||||||
|
.height(Length::Units(height as u16))
|
||||||
|
.resolve(Size::new(width, height));
|
||||||
|
|
||||||
|
layout::Node::new(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) -> event::Status {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let mut bounds_generator = self.button_bounds(bounds);
|
||||||
|
let state = tree.state.downcast_mut::<UniqueWidgetState>();
|
||||||
|
|
||||||
|
if bounds.contains(cursor_position) {
|
||||||
|
for (key, _) in self.state.buttons.iter() {
|
||||||
|
let bounds = bounds_generator();
|
||||||
|
if bounds.contains(cursor_position) {
|
||||||
|
// Record that the mouse is hovering over this button.
|
||||||
|
state.hovered = key;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.hovered = Key::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
event::Status::Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: iced::Point,
|
||||||
|
_viewport: &iced::Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> iced_native::mouse::Interaction {
|
||||||
|
let mut generator = self.button_bounds(layout.bounds());
|
||||||
|
|
||||||
|
if (0..self.state.buttons.len()).any(move |_| generator().contains(cursor_position)) {
|
||||||
|
iced_native::mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
iced_native::mouse::Interaction::Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &<Renderer as iced_native::Renderer>::Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor_position: iced::Point,
|
||||||
|
_viewport: &iced::Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_ref::<UniqueWidgetState>();
|
||||||
|
let appearance = theme.horizontal(&self.style);
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let button_amount = self.state.buttons.len();
|
||||||
|
|
||||||
|
let mut bounds_generator = self.button_bounds(bounds);
|
||||||
|
|
||||||
|
// 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: appearance.border_width,
|
||||||
|
border_color: appearance.border_color,
|
||||||
|
},
|
||||||
|
background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw each of the buttons in the widget.
|
||||||
|
for (num, (key, content)) in self.state.buttons.iter().enumerate() {
|
||||||
|
let bounds = bounds_generator();
|
||||||
|
|
||||||
|
let (button_appearance, font) = if self.state.active == key {
|
||||||
|
(appearance.button_active, &self.font_active)
|
||||||
|
} else if state.hovered == key {
|
||||||
|
(appearance.button_hover, &self.font_hovered)
|
||||||
|
} else {
|
||||||
|
(appearance.button_inactive, &self.font_inactive)
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = bounds.center_x();
|
||||||
|
let y = bounds.center_y();
|
||||||
|
|
||||||
|
// Render the background of the button.
|
||||||
|
if button_appearance.background.is_some() {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border_radius: if num == 0 {
|
||||||
|
button_appearance.border_radius_first
|
||||||
|
} else if num + 1 == button_amount {
|
||||||
|
button_appearance.border_radius_last
|
||||||
|
} else {
|
||||||
|
button_appearance.border_radius_middle
|
||||||
|
},
|
||||||
|
border_width: 0.0,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
},
|
||||||
|
button_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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the text in this button.
|
||||||
|
renderer.fill_text(iced_native::text::Text {
|
||||||
|
content: &content.text,
|
||||||
|
size: f32::from(renderer.default_size()),
|
||||||
|
bounds: Rectangle { x, y, ..bounds },
|
||||||
|
color: button_appearance.text_color,
|
||||||
|
font: font.clone(),
|
||||||
|
horizontal_alignment: Horizontal::Center,
|
||||||
|
vertical_alignment: Vertical::Center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b self,
|
||||||
|
_tree: &'b mut Tree,
|
||||||
|
_layout: iced_native::Layout<'_>,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> Option<iced_native::overlay::Element<'b, Message, Renderer>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> From<HorizontalSegmentedButton<'a, Message, Renderer>>
|
||||||
|
for Element<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
Message: 'static + Clone,
|
||||||
|
{
|
||||||
|
fn from(widget: HorizontalSegmentedButton<'a, Message, Renderer>) -> Self {
|
||||||
|
Self::new(widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,386 +44,19 @@
|
||||||
/// COSMIC configurations of [`SegmentedButton`].
|
/// COSMIC configurations of [`SegmentedButton`].
|
||||||
pub mod cosmic;
|
pub mod cosmic;
|
||||||
|
|
||||||
|
mod horizontal;
|
||||||
mod state;
|
mod state;
|
||||||
mod style;
|
mod style;
|
||||||
|
mod vertical;
|
||||||
|
|
||||||
pub use self::state::{ButtonContent, Key, SecondaryState, State, WidgetState};
|
pub use self::horizontal::{horizontal_segmented_button, HorizontalSegmentedButton};
|
||||||
|
pub use self::state::{ButtonContent, Key, SecondaryState, SharedWidgetState, State};
|
||||||
pub use self::style::{Appearance, ButtonAppearance, StyleSheet};
|
pub use self::style::{Appearance, ButtonAppearance, StyleSheet};
|
||||||
|
pub use self::vertical::{vertical_segmented_button, VerticalSegmentedButton};
|
||||||
|
|
||||||
use crate::widget::Orientation;
|
/// State that is maintained by each individual widget.
|
||||||
|
|
||||||
use derive_setters::Setters;
|
|
||||||
use iced::{
|
|
||||||
alignment::{Horizontal, Vertical},
|
|
||||||
event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, Size,
|
|
||||||
};
|
|
||||||
use iced_core::BorderRadius;
|
|
||||||
use iced_native::widget::tree;
|
|
||||||
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
|
||||||
|
|
||||||
/// Creates a widget that presents multiple conjoined buttons.
|
|
||||||
#[must_use]
|
|
||||||
pub fn segmented_button<Message, Renderer, Data>(
|
|
||||||
state: &State<Data>,
|
|
||||||
) -> SegmentedButton<Message, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
{
|
|
||||||
SegmentedButton::new(&state.inner)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State that is maintained by the widget internally.
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct PrivateWidgetState {
|
struct UniqueWidgetState {
|
||||||
/// The ID of the button that is being hovered. Defaults to null.
|
/// The ID of the button that is being hovered. Defaults to null.
|
||||||
hovered: Key,
|
hovered: Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget providing a conjoined set of linear buttons for choosing between.
|
|
||||||
///
|
|
||||||
/// The data for the widget comes from a [`State`] that is maintained the application.
|
|
||||||
#[derive(Setters)]
|
|
||||||
pub struct SegmentedButton<'a, Message, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
{
|
|
||||||
/// Contains application state also used for drawing.
|
|
||||||
#[setters(skip)]
|
|
||||||
state: &'a WidgetState,
|
|
||||||
/// Desired font for active tabs.
|
|
||||||
font_active: Renderer::Font,
|
|
||||||
/// Desired font for hovered tabs.
|
|
||||||
font_hovered: Renderer::Font,
|
|
||||||
/// Desired font for inactive tabs.
|
|
||||||
font_inactive: Renderer::Font,
|
|
||||||
/// Orientation of the buttons.
|
|
||||||
orientation: Orientation,
|
|
||||||
/// Desired width of the widget.
|
|
||||||
width: Length,
|
|
||||||
/// Desired height of the widget.
|
|
||||||
height: Length,
|
|
||||||
/// Padding around a button.
|
|
||||||
button_padding: [u16; 4],
|
|
||||||
/// Desired height of a button.
|
|
||||||
button_height: u16,
|
|
||||||
/// Desired spacing between buttons.
|
|
||||||
spacing: u16,
|
|
||||||
/// Style to draw the widget in.
|
|
||||||
#[setters(into)]
|
|
||||||
style: <Renderer::Theme as StyleSheet>::Style,
|
|
||||||
/// Emits the ID of the activated widget on selection.
|
|
||||||
#[setters(skip)]
|
|
||||||
on_activate: Option<Box<dyn Fn(Key) -> Message>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message, Renderer> SegmentedButton<'a, Message, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
{
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(state: &'a WidgetState) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
font_active: Renderer::Font::default(),
|
|
||||||
font_hovered: Renderer::Font::default(),
|
|
||||||
font_inactive: Renderer::Font::default(),
|
|
||||||
orientation: Orientation::Horizontal,
|
|
||||||
height: Length::Shrink,
|
|
||||||
width: Length::Fill,
|
|
||||||
button_padding: [4, 4, 4, 4],
|
|
||||||
button_height: 32,
|
|
||||||
spacing: 0,
|
|
||||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
|
||||||
on_activate: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits the ID of the activated widget on selection.
|
|
||||||
#[must_use]
|
|
||||||
pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self {
|
|
||||||
self.on_activate = Some(Box::from(on_activate));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a closure for generating the layout bounds of the buttons.
|
|
||||||
fn button_bounds(
|
|
||||||
&self,
|
|
||||||
bounds: Rectangle,
|
|
||||||
) -> stack_dst::ValueA<dyn FnMut() -> Rectangle, [usize; 4]> {
|
|
||||||
let button_amount = self.state.buttons.len() as f32;
|
|
||||||
match self.orientation {
|
|
||||||
Orientation::Horizontal => {
|
|
||||||
let width = bounds.width / button_amount;
|
|
||||||
let mut bounds = bounds;
|
|
||||||
bounds.width = width;
|
|
||||||
|
|
||||||
let closure = move || {
|
|
||||||
let clone = bounds;
|
|
||||||
bounds.x += width;
|
|
||||||
clone
|
|
||||||
};
|
|
||||||
|
|
||||||
stack_dst::ValueA::new_stable(closure, |p| p as _)
|
|
||||||
.ok()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
Orientation::Vertical => {
|
|
||||||
let height = bounds.height / button_amount;
|
|
||||||
let mut bounds = bounds;
|
|
||||||
bounds.height = height;
|
|
||||||
|
|
||||||
let closure = move || {
|
|
||||||
let clone = bounds;
|
|
||||||
bounds.y += height;
|
|
||||||
clone
|
|
||||||
};
|
|
||||||
|
|
||||||
stack_dst::ValueA::new_stable(closure, |p| p as _)
|
|
||||||
.ok()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn measure_button(
|
|
||||||
&self,
|
|
||||||
renderer: &Renderer,
|
|
||||||
text: &str,
|
|
||||||
text_size: u16,
|
|
||||||
bounds: Size,
|
|
||||||
) -> (f32, f32) {
|
|
||||||
let (mut w, mut h) = renderer.measure(text, text_size, Default::default(), bounds);
|
|
||||||
w += self.button_padding[0] as f32 + self.button_padding[2] as f32;
|
|
||||||
h += self.button_padding[1] as f32 + self.button_padding[3] as f32;
|
|
||||||
h = h.max(self.button_height as f32);
|
|
||||||
(w, h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for SegmentedButton<'a, Message, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Message: 'static + Clone,
|
|
||||||
{
|
|
||||||
fn tag(&self) -> tree::Tag {
|
|
||||||
tree::Tag::of::<PrivateWidgetState>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> tree::State {
|
|
||||||
tree::State::new(PrivateWidgetState::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn width(&self) -> Length {
|
|
||||||
self.width
|
|
||||||
}
|
|
||||||
|
|
||||||
fn height(&self) -> Length {
|
|
||||||
self.height
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
|
||||||
let mut width = 0.0f32;
|
|
||||||
let mut height = 0.0f32;
|
|
||||||
let mut limits = limits.width(self.width);
|
|
||||||
let text_size = renderer.default_size();
|
|
||||||
|
|
||||||
match self.orientation {
|
|
||||||
Orientation::Horizontal => {
|
|
||||||
for (_, content) in self.state.buttons.iter() {
|
|
||||||
let (w, h) =
|
|
||||||
self.measure_button(renderer, &content.text, text_size, limits.max());
|
|
||||||
width += w + f32::from(self.spacing * 2);
|
|
||||||
height = height.max(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
limits = limits.height(Length::Units(height as u16));
|
|
||||||
}
|
|
||||||
Orientation::Vertical => {
|
|
||||||
for (_, content) in self.state.buttons.iter() {
|
|
||||||
let (w, h) =
|
|
||||||
self.measure_button(renderer, &content.text, text_size, limits.max());
|
|
||||||
height += h + f32::from(self.spacing * 2);
|
|
||||||
width = width.max(w);
|
|
||||||
}
|
|
||||||
limits = limits.height(Length::Units(height as u16));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout::Node::new(limits.resolve(Size::new(width, height)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_event(
|
|
||||||
&mut self,
|
|
||||||
tree: &mut Tree,
|
|
||||||
event: Event,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
cursor_position: Point,
|
|
||||||
_renderer: &Renderer,
|
|
||||||
_clipboard: &mut dyn Clipboard,
|
|
||||||
shell: &mut Shell<'_, Message>,
|
|
||||||
) -> event::Status {
|
|
||||||
let bounds = layout.bounds();
|
|
||||||
let mut bounds_generator = self.button_bounds(bounds);
|
|
||||||
let state = tree.state.downcast_mut::<PrivateWidgetState>();
|
|
||||||
|
|
||||||
if bounds.contains(cursor_position) {
|
|
||||||
for (key, _) in self.state.buttons.iter() {
|
|
||||||
let bounds = bounds_generator();
|
|
||||||
if bounds.contains(cursor_position) {
|
|
||||||
// Record that the mouse is hovering over this button.
|
|
||||||
state.hovered = key;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.hovered = Key::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
event::Status::Ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mouse_interaction(
|
|
||||||
&self,
|
|
||||||
_tree: &Tree,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
cursor_position: iced::Point,
|
|
||||||
_viewport: &iced::Rectangle,
|
|
||||||
_renderer: &Renderer,
|
|
||||||
) -> iced_native::mouse::Interaction {
|
|
||||||
if layout.bounds().contains(cursor_position) {
|
|
||||||
iced_native::mouse::Interaction::Pointer
|
|
||||||
} else {
|
|
||||||
iced_native::mouse::Interaction::Idle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
tree: &Tree,
|
|
||||||
renderer: &mut Renderer,
|
|
||||||
theme: &<Renderer as iced_native::Renderer>::Theme,
|
|
||||||
_style: &renderer::Style,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
_cursor_position: iced::Point,
|
|
||||||
_viewport: &iced::Rectangle,
|
|
||||||
) {
|
|
||||||
let state = tree.state.downcast_ref::<PrivateWidgetState>();
|
|
||||||
let appearance = theme.appearance(&self.style, self.orientation);
|
|
||||||
let bounds = layout.bounds();
|
|
||||||
let button_amount = self.state.buttons.len();
|
|
||||||
|
|
||||||
let mut bounds_generator = self.button_bounds(bounds);
|
|
||||||
|
|
||||||
// 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: appearance.border_width,
|
|
||||||
border_color: appearance.border_color,
|
|
||||||
},
|
|
||||||
background,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw each of the buttons in the widget.
|
|
||||||
for (num, (key, content)) in self.state.buttons.iter().enumerate() {
|
|
||||||
let bounds = bounds_generator();
|
|
||||||
|
|
||||||
let (button_appearance, font) = if self.state.active == key {
|
|
||||||
(appearance.button_active, &self.font_active)
|
|
||||||
} else if state.hovered == key {
|
|
||||||
(appearance.button_hover, &self.font_hovered)
|
|
||||||
} else {
|
|
||||||
(appearance.button_inactive, &self.font_inactive)
|
|
||||||
};
|
|
||||||
|
|
||||||
let x = bounds.center_x();
|
|
||||||
let y = bounds.center_y();
|
|
||||||
|
|
||||||
// Render the background of the button.
|
|
||||||
if button_appearance.background.is_some() {
|
|
||||||
renderer.fill_quad(
|
|
||||||
renderer::Quad {
|
|
||||||
bounds,
|
|
||||||
border_radius: if num == 0 {
|
|
||||||
button_appearance.border_radius_first
|
|
||||||
} else if num + 1 == button_amount {
|
|
||||||
button_appearance.border_radius_last
|
|
||||||
} else {
|
|
||||||
button_appearance.border_radius_middle
|
|
||||||
},
|
|
||||||
border_width: 0.0,
|
|
||||||
border_color: Color::TRANSPARENT,
|
|
||||||
},
|
|
||||||
button_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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the text in this button.
|
|
||||||
renderer.fill_text(iced_native::text::Text {
|
|
||||||
content: &content.text,
|
|
||||||
size: f32::from(renderer.default_size()),
|
|
||||||
bounds: Rectangle { x, y, ..bounds },
|
|
||||||
color: button_appearance.text_color,
|
|
||||||
font: font.clone(),
|
|
||||||
horizontal_alignment: Horizontal::Center,
|
|
||||||
vertical_alignment: Vertical::Center,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overlay<'b>(
|
|
||||||
&'b self,
|
|
||||||
_tree: &'b mut Tree,
|
|
||||||
_layout: iced_native::Layout<'_>,
|
|
||||||
_renderer: &Renderer,
|
|
||||||
) -> Option<iced_native::overlay::Element<'b, Message, Renderer>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message, Renderer> From<SegmentedButton<'a, Message, Renderer>>
|
|
||||||
for Element<'a, Message, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Message: 'static + Clone,
|
|
||||||
{
|
|
||||||
fn from(widget: SegmentedButton<'a, Message, Renderer>) -> Self {
|
|
||||||
Self::new(widget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use slotmap::{SecondaryMap, SlotMap};
|
use slotmap::{SecondaryMap, SlotMap};
|
||||||
use std::{borrow::Cow, cell::Cell};
|
use std::borrow::Cow;
|
||||||
|
|
||||||
slotmap::new_key_type! {
|
slotmap::new_key_type! {
|
||||||
/// An ID for a segmented button
|
/// An ID for a segmented button
|
||||||
|
|
@ -11,8 +11,8 @@ slotmap::new_key_type! {
|
||||||
|
|
||||||
/// Contains all state for interacting with a segmented button.
|
/// Contains all state for interacting with a segmented button.
|
||||||
pub struct State<Data> {
|
pub struct State<Data> {
|
||||||
/// State that is shared with widget drawing.
|
/// State that is shareable across widget(s).
|
||||||
pub inner: WidgetState,
|
pub inner: SharedWidgetState,
|
||||||
|
|
||||||
/// State unique to the application.
|
/// State unique to the application.
|
||||||
pub data: SecondaryState<Data>,
|
pub data: SecondaryState<Data>,
|
||||||
|
|
@ -21,7 +21,7 @@ pub struct State<Data> {
|
||||||
impl<Data> Default for State<Data> {
|
impl<Data> Default for State<Data> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: WidgetState::default(),
|
inner: SharedWidgetState::default(),
|
||||||
data: SecondaryState::default(),
|
data: SecondaryState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,15 +29,12 @@ impl<Data> Default for State<Data> {
|
||||||
|
|
||||||
/// State which is most useful to the widget.
|
/// State which is most useful to the widget.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct WidgetState {
|
pub struct SharedWidgetState {
|
||||||
/// The content used for drawing segmented buttons.
|
/// The content used for drawing segmented buttons.
|
||||||
pub buttons: SlotMap<Key, ButtonContent>,
|
pub buttons: SlotMap<Key, ButtonContent>,
|
||||||
|
|
||||||
/// The actively-selected segmented button.
|
/// The actively-selected segmented button.
|
||||||
pub active: Key,
|
pub active: Key,
|
||||||
|
|
||||||
/// The button currently hovered.
|
|
||||||
pub hovered: Cell<Key>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State which is most useful to the application.
|
/// State which is most useful to the application.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::widget::Orientation;
|
|
||||||
use iced_core::{Background, BorderRadius, Color};
|
use iced_core::{Background, BorderRadius, Color};
|
||||||
|
|
||||||
/// The appearance of a segmented button.
|
/// The appearance of a segmented button.
|
||||||
|
|
@ -32,6 +31,9 @@ pub trait StyleSheet {
|
||||||
/// The supported style of the [`StyleSheet`].
|
/// The supported style of the [`StyleSheet`].
|
||||||
type Style: Default;
|
type Style: Default;
|
||||||
|
|
||||||
/// The [`Appearance`] of the segmented button.
|
/// The horizontal [`Appearance`] of the segmented button.
|
||||||
fn appearance(&self, style: &Self::Style, orientation: Orientation) -> Appearance;
|
fn horizontal(&self, style: &Self::Style) -> Appearance;
|
||||||
|
|
||||||
|
/// The vertical [`Appearance`] of the segmented button.
|
||||||
|
fn vertical(&self, style: &Self::Style) -> Appearance;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
347
src/widget/segmented_button/vertical.rs
Normal file
347
src/widget/segmented_button/vertical.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
use super::state::{Key, SharedWidgetState, State};
|
||||||
|
use super::style::StyleSheet;
|
||||||
|
use super::UniqueWidgetState;
|
||||||
|
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use iced::{
|
||||||
|
alignment::{Horizontal, Vertical},
|
||||||
|
event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, Size,
|
||||||
|
};
|
||||||
|
use iced_core::BorderRadius;
|
||||||
|
use iced_native::widget::tree;
|
||||||
|
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
||||||
|
|
||||||
|
/// Creates a [`VerticalSegmentedButton`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn vertical_segmented_button<Message, Renderer, Data>(
|
||||||
|
state: &State<Data>,
|
||||||
|
) -> VerticalSegmentedButton<Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
VerticalSegmentedButton::new(&state.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget providing a conjoined set of linear buttons for choosing between.
|
||||||
|
///
|
||||||
|
/// The data for the widget comes from a [`State`] that is maintained the application.
|
||||||
|
#[derive(Setters)]
|
||||||
|
pub struct VerticalSegmentedButton<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
/// Contains application state also used for drawing.
|
||||||
|
#[setters(skip)]
|
||||||
|
state: &'a SharedWidgetState,
|
||||||
|
/// Desired font for active tabs.
|
||||||
|
font_active: Renderer::Font,
|
||||||
|
/// Desired font for hovered tabs.
|
||||||
|
font_hovered: Renderer::Font,
|
||||||
|
/// Desired font for inactive tabs.
|
||||||
|
font_inactive: Renderer::Font,
|
||||||
|
/// Desired width of the widget.
|
||||||
|
width: Length,
|
||||||
|
/// Desired height of the widget.
|
||||||
|
height: Length,
|
||||||
|
/// Padding around a button.
|
||||||
|
button_padding: [u16; 4],
|
||||||
|
/// Desired height of a button.
|
||||||
|
button_height: u16,
|
||||||
|
/// Desired spacing between buttons.
|
||||||
|
spacing: u16,
|
||||||
|
/// Style to draw the widget in.
|
||||||
|
#[setters(into)]
|
||||||
|
style: <Renderer::Theme as StyleSheet>::Style,
|
||||||
|
/// Emits the ID of the activated widget on selection.
|
||||||
|
#[setters(skip)]
|
||||||
|
on_activate: Option<Box<dyn Fn(Key) -> Message>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> VerticalSegmentedButton<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(state: &'a SharedWidgetState) -> Self {
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
font_active: Renderer::Font::default(),
|
||||||
|
font_hovered: Renderer::Font::default(),
|
||||||
|
font_inactive: Renderer::Font::default(),
|
||||||
|
height: Length::Shrink,
|
||||||
|
width: Length::Fill,
|
||||||
|
button_padding: [4, 4, 4, 4],
|
||||||
|
button_height: 32,
|
||||||
|
spacing: 0,
|
||||||
|
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||||
|
on_activate: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits the ID of the activated widget on selection.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self {
|
||||||
|
self.on_activate = Some(Box::from(on_activate));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a closure for generating the layout bounds of the buttons.
|
||||||
|
fn button_bounds(&self, bounds: Rectangle) -> impl FnMut() -> Rectangle {
|
||||||
|
let button_amount = self.state.buttons.len();
|
||||||
|
let height = bounds.height / button_amount as f32;
|
||||||
|
let spacing = self.spacing as f32;
|
||||||
|
let half = spacing / 2.0;
|
||||||
|
let mut bounds = bounds;
|
||||||
|
bounds.height = height;
|
||||||
|
let mut counter = 1;
|
||||||
|
|
||||||
|
move || {
|
||||||
|
let mut clone = bounds;
|
||||||
|
if counter == 1 {
|
||||||
|
clone.height -= half;
|
||||||
|
} else if counter == button_amount {
|
||||||
|
clone.y += spacing;
|
||||||
|
clone.height -= spacing;
|
||||||
|
} else {
|
||||||
|
clone.y += half;
|
||||||
|
clone.height -= half;
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds.y += height;
|
||||||
|
counter += 1;
|
||||||
|
clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_button(
|
||||||
|
&self,
|
||||||
|
renderer: &Renderer,
|
||||||
|
text: &str,
|
||||||
|
text_size: u16,
|
||||||
|
bounds: Size,
|
||||||
|
) -> (f32, f32) {
|
||||||
|
let (mut w, mut h) = renderer.measure(text, text_size, Default::default(), bounds);
|
||||||
|
w += self.button_padding[0] as f32 + self.button_padding[2] as f32;
|
||||||
|
h += self.button_padding[1] as f32 + self.button_padding[3] as f32;
|
||||||
|
h = h.max(self.button_height as f32);
|
||||||
|
(w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> Widget<Message, Renderer>
|
||||||
|
for VerticalSegmentedButton<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
Message: 'static + Clone,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<UniqueWidgetState>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(UniqueWidgetState::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(&self) -> Length {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> Length {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||||
|
let mut width = 0.0f32;
|
||||||
|
let mut height = 0.0f32;
|
||||||
|
let limits = limits.width(self.width);
|
||||||
|
let text_size = renderer.default_size();
|
||||||
|
|
||||||
|
for (_, content) in self.state.buttons.iter() {
|
||||||
|
let (w, h) = self.measure_button(renderer, &content.text, text_size, limits.max());
|
||||||
|
height += h + f32::from(self.spacing * 2);
|
||||||
|
width = width.max(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = limits
|
||||||
|
.height(Length::Units(height as u16))
|
||||||
|
.resolve(Size::new(width, height));
|
||||||
|
|
||||||
|
layout::Node::new(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: Point,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
) -> event::Status {
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let mut bounds_generator = self.button_bounds(bounds);
|
||||||
|
let state = tree.state.downcast_mut::<UniqueWidgetState>();
|
||||||
|
|
||||||
|
if bounds.contains(cursor_position) {
|
||||||
|
for (key, _) in self.state.buttons.iter() {
|
||||||
|
let bounds = bounds_generator();
|
||||||
|
if bounds.contains(cursor_position) {
|
||||||
|
// Record that the mouse is hovering over this button.
|
||||||
|
state.hovered = key;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.hovered = Key::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
event::Status::Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
_tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: iced::Point,
|
||||||
|
_viewport: &iced::Rectangle,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> iced_native::mouse::Interaction {
|
||||||
|
let mut generator = self.button_bounds(layout.bounds());
|
||||||
|
|
||||||
|
if (0..self.state.buttons.len()).any(move |_| generator().contains(cursor_position)) {
|
||||||
|
iced_native::mouse::Interaction::Pointer
|
||||||
|
} else {
|
||||||
|
iced_native::mouse::Interaction::Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &<Renderer as iced_native::Renderer>::Theme,
|
||||||
|
_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
_cursor_position: iced::Point,
|
||||||
|
_viewport: &iced::Rectangle,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_ref::<UniqueWidgetState>();
|
||||||
|
let appearance = theme.vertical(&self.style);
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let button_amount = self.state.buttons.len();
|
||||||
|
|
||||||
|
let mut bounds_generator = self.button_bounds(bounds);
|
||||||
|
|
||||||
|
// 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: appearance.border_width,
|
||||||
|
border_color: appearance.border_color,
|
||||||
|
},
|
||||||
|
background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw each of the buttons in the widget.
|
||||||
|
for (num, (key, content)) in self.state.buttons.iter().enumerate() {
|
||||||
|
let bounds = bounds_generator();
|
||||||
|
|
||||||
|
let (button_appearance, font) = if self.state.active == key {
|
||||||
|
(appearance.button_active, &self.font_active)
|
||||||
|
} else if state.hovered == key {
|
||||||
|
(appearance.button_hover, &self.font_hovered)
|
||||||
|
} else {
|
||||||
|
(appearance.button_inactive, &self.font_inactive)
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = bounds.center_x();
|
||||||
|
let y = bounds.center_y();
|
||||||
|
|
||||||
|
// Render the background of the button.
|
||||||
|
if button_appearance.background.is_some() {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds,
|
||||||
|
border_radius: if num == 0 {
|
||||||
|
button_appearance.border_radius_first
|
||||||
|
} else if num + 1 == button_amount {
|
||||||
|
button_appearance.border_radius_last
|
||||||
|
} else {
|
||||||
|
button_appearance.border_radius_middle
|
||||||
|
},
|
||||||
|
border_width: 0.0,
|
||||||
|
border_color: Color::TRANSPARENT,
|
||||||
|
},
|
||||||
|
button_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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the text in this button.
|
||||||
|
renderer.fill_text(iced_native::text::Text {
|
||||||
|
content: &content.text,
|
||||||
|
size: f32::from(renderer.default_size()),
|
||||||
|
bounds: Rectangle { x, y, ..bounds },
|
||||||
|
color: button_appearance.text_color,
|
||||||
|
font: font.clone(),
|
||||||
|
horizontal_alignment: Horizontal::Center,
|
||||||
|
vertical_alignment: Vertical::Center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b self,
|
||||||
|
_tree: &'b mut Tree,
|
||||||
|
_layout: iced_native::Layout<'_>,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
) -> Option<iced_native::overlay::Element<'b, Message, Renderer>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Renderer> From<VerticalSegmentedButton<'a, Message, Renderer>>
|
||||||
|
for Element<'a, Message, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a,
|
||||||
|
Renderer::Theme: StyleSheet,
|
||||||
|
Message: 'static + Clone,
|
||||||
|
{
|
||||||
|
fn from(widget: VerticalSegmentedButton<'a, Message, Renderer>) -> Self {
|
||||||
|
Self::new(widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue