feat!(widget): rewrite button & icon widget APIs

This commit is contained in:
Michael Aaron Murphy 2023-09-01 07:29:19 +02:00 committed by Michael Murphy
parent 18debe546d
commit 4e4eeaac12
60 changed files with 2191 additions and 1113 deletions

View file

@ -125,9 +125,9 @@ impl cosmic::Application for App {
let text = cosmic::widget::text(page_content);
let centered = iced::widget::container(text)
let centered = cosmic::widget::container(text)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.height(iced::Length::Shrink)
.align_x(iced::alignment::Horizontal::Center)
.align_y(iced::alignment::Vertical::Center);

View file

@ -6,5 +6,6 @@ edition = "2021"
publish = false
[dependencies]
libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] }
cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c4895f6", default-features = false, features = ["libcosmic", "once_cell"] }
libcosmic = { path = "../..", features = ["wayland", "tokio"] }
cosmic-time = { git = "https://github.com/pop-os/cosmic-time", branch = "icon-color", default-features = false, features = ["libcosmic", "once_cell"] }
# cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"]}

View file

@ -1,9 +1,3 @@
# COSMIC
An example of the COSMIC design system.
# Deprecated
All the example code is located in the __[`main`](src/main.rs)__ file.
You can run it with `cargo run`:
```
cargo run --package cosmic --release
```
This example will be removed once its contents are migrated to the design demo.

View file

@ -10,22 +10,21 @@ use cosmic::{
},
iced_futures::Subscription,
iced_style::application,
iced_widget::text,
prelude::*,
theme::{self, Theme},
widget::{
button, cosmic_container, header_bar, icon, inline_input, nav_bar, nav_bar_toggle,
rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate},
scrollable, search_input, secure_input, segmented_button, segmented_selection, settings,
text_input, IconSource,
text, text_input,
},
Element, ElementExt,
Element,
};
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
use std::{
sync::atomic::{AtomicU32, Ordering},
vec,
};
use theme::Button as ButtonTheme;
static DEBUG_TOGGLER: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
static TOGGLER: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
@ -138,7 +137,7 @@ impl Window {
self.nav_bar_pages
.insert()
.text(page.title())
.icon(IconSource::from(page.icon_name()))
.icon(icon::handle::from_name(page.icon_name()).icon())
.data(page)
}
@ -373,15 +372,6 @@ impl Application for Window {
}
if !nav_bar_toggled {
let secondary = button(ButtonTheme::Secondary)
.text("Secondary")
.on_press(Message::ButtonPressed);
let secondary = if let Some(tracker) = self.rectangle_tracker.as_ref() {
tracker.container(0, secondary).into()
} else {
secondary.into()
};
let content: Element<_> = settings::view_column(vec![
settings::view_section("Debug")
.add(settings::item(
@ -396,34 +386,6 @@ impl Application for Window {
)),
))
.into(),
settings::view_section("Buttons")
.add(settings::item_row(vec![
button(ButtonTheme::Primary)
.text("Primary")
.on_press(Message::ButtonPressed)
.into(),
secondary,
button(ButtonTheme::Positive)
.text("Positive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Destructive)
.text("Destructive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Text)
.text("Text")
.on_press(Message::ButtonPressed)
.into(),
]))
.add(settings::item_row(vec![
button(ButtonTheme::Primary).text("Primary").into(),
button(ButtonTheme::Secondary).text("Secondary").into(),
button(ButtonTheme::Positive).text("Positive").into(),
button(ButtonTheme::Destructive).text("Destructive").into(),
button(ButtonTheme::Text).text("Text").into(),
]))
.into(),
settings::view_section("Controls")
.add(settings::item(
"Toggler",
@ -567,6 +529,7 @@ impl Application for Window {
fn style(&self) -> <Self::Theme as cosmic::iced_style::application::StyleSheet>::Style {
cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance {
background_color: Color::TRANSPARENT,
icon_color: theme.cosmic().on_bg_color().into(),
text_color: theme.cosmic().on_bg_color().into(),
}))
}

View file

@ -8,9 +8,9 @@ publish = false
[dependencies]
apply = "0.3.0"
fraction = "0.13.0"
libcosmic = { path = "../..", default-features = false, features = ["debug", "winit"] }
libcosmic = { path = "../..", features = ["debug", "winit", "tokio"] }
once_cell = "1.18"
slotmap = "1.0.6"
env_logger = "0.10"
log = "0.4.17"
cosmic-time = { git = "https://github.com/pop-os/cosmic-time", rev="c4895f6", default-features = false, features = ["libcosmic", "once_cell"] }
cosmic-time = { git = "https://github.com/pop-os/cosmic-time", branch="icon-color", default-features = false, features = ["libcosmic", "once_cell"] }

View file

@ -1,9 +1,3 @@
# COSMIC
An example of the COSMIC design system.
# Deprecated
All the example code is located in the __[`main`](src/main.rs)__ file.
You can run it with `cargo run`:
```
cargo run --package cosmic --release
```
This example will be removed once its contents are migrated to the design demo.

View file

@ -13,12 +13,13 @@ use cosmic::{
window::{self, close, drag, minimize, toggle_maximize},
},
keyboard_nav,
prelude::*,
theme::{self, Theme},
widget::{
header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings,
warning, IconSource,
button, header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button,
settings, warning,
},
Element, ElementExt,
Element,
};
use cosmic_time::{Instant, Timeline};
use std::{
@ -224,7 +225,7 @@ impl Window {
self.nav_bar
.insert()
.text(page.title())
.icon(IconSource::from(page.icon_name()))
.icon(icon::handle::from_name(page.icon_name()).icon())
.secondary(&mut self.nav_id_to_page, page)
}
@ -247,14 +248,10 @@ impl Window {
) -> Element<Message> {
let page = sub_page.parent_page();
column!(
iced::widget::Button::new(row!(
icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink),
text(page.title()).size(14),
))
.padding(0)
.style(theme::Button::Link)
// .id(BTN.clone())
.on_press(Message::from(page)),
button::icon(icon::handle::from_name("go-previous-symbolic").size(16))
.label(page.title())
.padding(0)
.on_press(Message::from(page)),
row!(
text(sub_page.title()).size(28),
horizontal_space(Length::Fill),
@ -276,8 +273,9 @@ impl Window {
iced::widget::Button::new(
container(
settings::item_row(vec![
icon(sub_page.icon_name(), 20)
.style(theme::Svg::Symbolic)
icon::handle::from_name(sub_page.icon_name())
.size(20)
.icon()
.into(),
column!(
text(sub_page.title()).size(14),
@ -286,8 +284,9 @@ impl Window {
.spacing(2)
.into(),
horizontal_space(iced::Length::Fill).into(),
icon("go-next-symbolic", 20)
.style(theme::Svg::Symbolic)
icon::handle::from_name("go-next-symbolic")
.size(20)
.icon()
.into(),
])
.spacing(16),
@ -296,7 +295,7 @@ impl Window {
.style(theme::Container::custom(list::column::style)),
)
.padding(0)
.style(theme::Button::Transparent)
.style(theme::IcedButton::Transparent)
.on_press(Message::from(sub_page.into_page()))
// .id(BTN.clone())
.into()

View file

@ -7,8 +7,8 @@ use cosmic::{
iced::{id, Alignment, Length},
theme::{self, Button as ButtonTheme, ThemeType},
widget::{
button, container, icon, segmented_button, segmented_selection, settings, spin_button,
toggler, view_switcher,
button, cosmic_container::container, icon, segmented_button, segmented_selection, settings,
spin_button, toggler, view_switcher,
},
Element,
};
@ -186,7 +186,7 @@ impl State {
Message::IconTheme(key) => {
self.icon_themes.activate(key);
if let Some(theme) = self.icon_themes.text(key) {
cosmic::icon_theme::set_default(theme);
cosmic::icon_theme::set_default(theme.to_owned());
}
}
Message::InputChanged(s) => {
@ -255,45 +255,13 @@ impl State {
"Scaling Factor",
spin_button(&window.scale_factor_string, Message::ScalingFactor),
))
.add(settings::item_row(vec![button(ButtonTheme::Destructive)
.on_press(Message::ToggleWarning)
.custom(vec![
icon("dialog-warning-symbolic", 16)
.style(theme::Svg::SymbolicPrimary)
.into(),
text("Do Not Touch").into(),
])
.into()]))
.into(),
settings::view_section("Buttons")
.add(settings::item_row(vec![
button(ButtonTheme::Primary)
.text("Primary")
.on_press(Message::ButtonPressed)
cosmic::widget::button::destructive("Do not Touch")
.trailing_icon(
icon::handle::from_name("dialog-warning-symbolic").size(16),
)
.on_press(Message::ToggleWarning)
.into(),
button(ButtonTheme::Secondary)
.text("Secondary")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Positive)
.text("Positive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Destructive)
.text("Destructive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Text)
.text("Text")
.on_press(Message::ButtonPressed)
.into(),
]))
.add(settings::item_row(vec![
button(ButtonTheme::Primary).text("Primary").into(),
button(ButtonTheme::Secondary).text("Secondary").into(),
button(ButtonTheme::Positive).text("Positive").into(),
button(ButtonTheme::Destructive).text("Destructive").into(),
button(ButtonTheme::Text).text("Text").into(),
]))
.into(),
settings::view_section("Controls")
@ -454,9 +422,13 @@ impl State {
"Primary container with some text and a couple icons testing default fallbacks"
)
.size(24),
icon("microphone-sensitivity-high-symbolic-test", 24)
.style(cosmic::theme::Svg::SymbolicActive),
icon("microphone-sensitivity-high-symbolic-test", 16).default_fallbacks(false)
icon::handle::from_name("microphone-sensitivity-high-symbolic-test")
.size(24)
.icon(),
icon::handle::from_name("microphone-sensitivity-high-symbolic-test")
.size(24)
.fallback(false)
.icon(),
])
.layer(cosmic_theme::Layer::Primary)
.padding(8)
@ -475,9 +447,7 @@ impl State {
.iter()
.enumerate()
.map(|(i, c)| column![
button(cosmic::theme::Button::Text)
.text("Delete me")
.on_press(Message::DeleteCard(i)),
button::text("Delete me").on_press(Message::DeleteCard(i)),
text(c).size(24).width(Length::Fill)
]
.into())

View file

@ -1,7 +1,7 @@
use cosmic::iced::widget::{horizontal_space, row};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, segmented_button, view_switcher};
use cosmic::{theme, Element};
use cosmic::widget::{button, icon, segmented_button, view_switcher};
use cosmic::{theme, Apply, Element};
use slotmap::Key;
#[derive(Clone, Copy, Debug)]
@ -66,8 +66,9 @@ impl State {
.on_close(Message::Close)
.width(Length::Shrink);
let new_tab_button = button(theme::Button::Text)
.icon(theme::Svg::Symbolic, "tab-new-symbolic", 20)
let new_tab_button = icon::handle::from_name("tab-new-symbolic")
.size(20)
.apply(button::icon)
.on_press(Message::AddNew);
let tab_header = row!(tabs, new_tab_button).align_items(Alignment::Center);

View file

@ -62,7 +62,7 @@ impl State {
window.parent_page_button(SystemAndAccountsPage::About),
row!(
horizontal_space(Length::Fill),
icon("distributor-logo", 78),
icon::handle::from_name("distributor-logo").size(78).icon(),
horizontal_space(Length::Fill),
)
.into(),

View file

@ -7,6 +7,7 @@ use apply::Apply;
use cosmic::app::{Command, Core, Settings};
use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced_core::Length;
use cosmic::widget::button;
use cosmic::{executor, iced, ApplicationExt, Element};
use tokio::io::AsyncReadExt;
use url::Url;
@ -82,10 +83,7 @@ impl cosmic::Application for App {
fn header_end(&self) -> Vec<Element<Self::Message>> {
// Places a button the header to create open dialogs.
vec![cosmic::widget::button(cosmic::theme::Button::Primary)
.text("Open")
.on_press(Message::OpenFile)
.into()]
vec![button::suggested("Open").on_press(Message::OpenFile).into()]
}
fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {

2
iced

@ -1 +1 @@
Subproject commit 2ead0da06f6da58b01e107104808b45d6fb61e85
Subproject commit 8b2389f144966a5f9b60ab778c1073748fee5e70

6
res/external-link.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon Symbolic">
<path id="Vector" d="M8 1V3H11.586L7.385 7.201C7.146 7.386 7.023 7.692 7.018 8H7V9H8V8.99C8.308 8.987 8.614 8.86 8.799 8.615L12.999 4.414V8L14.999 8.006L15 1H8Z" fill="#232323"/>
<path id="Vector 42" d="M12 9V13H3V4H7" stroke="black" stroke-width="2" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View file

@ -127,7 +127,7 @@ impl CosmicAppletHelper {
pub fn icon_button<'a, Message: 'static>(
&self,
icon_name: &'a str,
) -> iced::widget::Button<'a, Message, Renderer> {
) -> crate::widget::Button<'a, Message, Renderer> {
crate::widget::button(theme::Button::Text)
.icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0)
.padding(8)

View file

@ -110,6 +110,7 @@ where
} else {
theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance {
background_color: iced_core::Color::TRANSPARENT,
icon_color: theme.cosmic().on_bg_color().into(),
text_color: theme.cosmic().on_bg_color().into(),
}))
}

View file

@ -41,9 +41,9 @@ pub mod message {
pub use self::command::Command;
pub use self::core::Core;
pub use self::settings::Settings;
use crate::prelude::*;
use crate::theme::THEME;
use crate::widget::nav_bar;
use crate::{Element, ElementExt};
use apply::Apply;
use iced::Subscription;
use iced::{window, Application as IcedApplication};
@ -261,91 +261,90 @@ impl<App: Application> ApplicationExt for App {
}
/// Creates the view for the main window.
fn view_main<'a>(&'a self) -> Element<'a, Message<Self::Message>> {
fn view_main(&self) -> Element<Message<Self::Message>> {
let core = self.core();
let is_condensed = core.is_condensed();
let mut main: Vec<Element<'a, Message<Self::Message>>> = Vec::with_capacity(2);
if core.window.show_headerbar {
main.push({
let mut header = crate::widget::header_bar()
.title(self.title())
.on_drag(Message::Cosmic(cosmic::Message::Drag))
.on_close(Message::Cosmic(cosmic::Message::Close));
crate::widget::column::with_capacity(2)
.push_maybe(if core.window.show_headerbar {
Some({
let mut header = crate::widget::header_bar()
.title(self.title())
.on_drag(Message::Cosmic(cosmic::Message::Drag))
.on_close(Message::Cosmic(cosmic::Message::Close));
if self.nav_model().is_some() {
let toggle = crate::widget::nav_bar_toggle()
.active(core.nav_bar_active())
.on_toggle(if is_condensed {
Message::Cosmic(cosmic::Message::ToggleNavBarCondensed)
} else {
Message::Cosmic(cosmic::Message::ToggleNavBar)
});
if self.nav_model().is_some() {
let toggle = crate::widget::nav_bar_toggle()
.active(core.nav_bar_active())
.on_toggle(if is_condensed {
Message::Cosmic(cosmic::Message::ToggleNavBarCondensed)
} else {
Message::Cosmic(cosmic::Message::ToggleNavBar)
});
header = header.start(toggle);
}
if core.window.show_maximize {
header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize));
}
if core.window.show_minimize {
header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize));
}
for element in self.header_start() {
header = header.start(element.map(Message::App));
}
for element in self.header_center() {
header = header.center(element.map(Message::App));
}
for element in self.header_end() {
header = header.end(element.map(Message::App));
}
Element::from(header).debug(core.debug)
});
}
// The content element contains every element beneath the header.
main.push(
iced::widget::row({
let mut widgets = Vec::with_capacity(2);
// Insert nav bar onto the left side of the window.
if core.nav_bar_active() {
if let Some(nav_model) = self.nav_model() {
let mut nav = crate::widget::nav_bar(nav_model, |entity| {
Message::Cosmic(cosmic::Message::NavBar(entity))
});
if !is_condensed {
nav = nav.max_width(300);
}
widgets.push(nav.apply(Element::from).debug(core.debug));
header = header.start(toggle);
}
}
if self.nav_model().is_none() || core.show_content() {
let main_content = self.view().debug(core.debug).map(Message::App);
if core.window.show_maximize {
header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize));
}
widgets.push(main_content);
}
if core.window.show_minimize {
header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize));
}
widgets
for element in self.header_start() {
header = header.start(element.map(Message::App));
}
for element in self.header_center() {
header = header.center(element.map(Message::App));
}
for element in self.header_end() {
header = header.end(element.map(Message::App));
}
header
})
} else {
None
})
.spacing(8)
.apply(iced::widget::container)
.padding([0, 8, 8, 8])
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.style(crate::theme::Container::Background)
.into(),
);
// The content element contains every element beneath the header.
.push(
crate::widget::row::with_children({
let mut widgets = Vec::with_capacity(2);
iced::widget::column(main).into()
// Insert nav bar onto the left side of the window.
if core.nav_bar_active() {
if let Some(nav_model) = self.nav_model() {
let mut nav = crate::widget::nav_bar(nav_model, |entity| {
Message::Cosmic(cosmic::Message::NavBar(entity))
});
if !is_condensed {
nav = nav.max_width(300);
}
widgets.push(nav.apply(Element::from).debug(core.debug));
}
}
if self.nav_model().is_none() || core.show_content() {
let main_content = self.view().debug(core.debug).map(Message::App);
widgets.push(main_content);
}
widgets
})
.spacing(8)
.apply(crate::widget::container)
.padding([0, 8, 8, 8])
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.style(crate::theme::Container::Background),
)
.into()
}
}

View file

@ -10,6 +10,7 @@ use iced_core::Font;
/// Configure a new COSMIC application.
#[allow(clippy::struct_excessive_bools)]
#[must_use]
#[derive(derive_setters::Setters)]
pub struct Settings {
/// Produces a smoother result in some widgets, at a performance cost.

113
src/theme/button.rs Normal file
View file

@ -0,0 +1,113 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic_theme::Component;
use iced_core::{Background, Color};
use palette::{rgb::Rgb, Alpha};
use crate::{
app,
widget::button::{Appearance, StyleSheet},
};
#[derive(Copy, Clone, Debug, Default)]
pub enum Button {
Destructive,
Link,
Icon,
#[default]
Standard,
Suggested,
Text,
}
pub fn appearance(
theme: &crate::Theme,
focused: bool,
style: &Button,
color: fn(&Component<Alpha<Rgb, f32>>) -> Color,
) -> Appearance {
let cosmic = theme.cosmic();
let mut corner_radii = &cosmic.corner_radii.radius_xl;
let mut appearance = Appearance::new();
match style {
Button::Standard => {
let component = &theme.current_container().component;
appearance.background = Some(Background::Color(color(component)));
appearance.text_color = component.on.into();
}
Button::Icon | Button::Text => {
let component = &cosmic.text_button;
appearance.background = None;
appearance.text_color = component.on.into();
}
Button::Suggested => {
let component = &cosmic.accent_button;
appearance.background = Some(Background::Color(color(component)));
appearance.icon_color = Some(component.on.into());
appearance.text_color = component.on.into();
}
Button::Destructive => {
let component = &cosmic.destructive_button;
appearance.background = Some(Background::Color(color(component)));
appearance.icon_color = Some(component.on.into());
appearance.text_color = component.on.into();
}
Button::Link => {
appearance.background = None;
appearance.icon_color = Some(cosmic.accent.base.into());
appearance.text_color = cosmic.accent.base.into();
corner_radii = &cosmic.corner_radii.radius_0;
}
}
appearance.border_radius = (*corner_radii).into();
if focused {
appearance.outline_width = 1.0;
appearance.outline_color = cosmic.accent.base.into();
appearance.border_width = 2.0;
appearance.border_color = Color::TRANSPARENT;
}
appearance
}
impl StyleSheet for crate::Theme {
type Style = Button;
fn active(&self, focused: bool, style: &Self::Style) -> Appearance {
appearance(self, focused, style, |component| component.base.into())
}
fn disabled(&self, style: &Self::Style) -> Appearance {
appearance(self, false, style, |component| {
let mut color = Color::from(component.base);
color.a *= 0.5;
color
})
}
fn drop_target(&self, style: &Self::Style) -> Appearance {
let mut appearance = self.active(false, style);
appearance
}
fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance {
appearance(self, focused, style, |component| component.hover.into())
}
fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance {
appearance(self, focused, style, |component| component.pressed.into())
}
fn selected(&self, focused: bool, style: &Self::Style) -> Appearance {
appearance(self, focused, style, |component| component.selected.into())
}
}

View file

@ -4,17 +4,18 @@
//! Use COSMIC's themes and styles.
pub mod expander;
mod button;
pub use self::button::Button;
mod segmented_button;
pub use self::segmented_button::SegmentedButton;
use std::cell::RefCell;
use std::f32::consts::PI;
use std::hash::Hash;
use std::hash::Hasher;
use std::rc::Rc;
use std::sync::Arc;
pub use self::segmented_button::SegmentedButton;
use cosmic_config::config_subscription;
use cosmic_config::CosmicConfigEntry;
use cosmic_theme::composite::over;
@ -26,7 +27,7 @@ use iced_core::BorderRadius;
use iced_core::Radians;
use iced_futures::Subscription;
use iced_style::application;
use iced_style::button;
use iced_style::button as iced_button;
use iced_style::checkbox;
use iced_style::container;
use iced_style::menu;
@ -195,6 +196,7 @@ impl application::StyleSheet for Theme {
match style {
Application::Default => application::Appearance {
icon_color: cosmic.bg_color().into(),
background_color: cosmic.bg_color().into(),
text_color: cosmic.on_bg_color().into(),
},
@ -203,13 +205,13 @@ impl application::StyleSheet for Theme {
}
}
/*
* TODO: Button
*/
pub enum Button {
/// Styles for the button widget from iced-rs.
#[derive(Default)]
pub enum IcedButton {
Deactivated,
Destructive,
Positive,
#[default]
Primary,
Secondary,
Text,
@ -218,110 +220,104 @@ pub enum Button {
Transparent,
Card,
Custom {
active: Box<dyn Fn(&Theme) -> button::Appearance>,
hover: Box<dyn Fn(&Theme) -> button::Appearance>,
active: Box<dyn Fn(&Theme) -> iced_button::Appearance>,
hover: Box<dyn Fn(&Theme) -> iced_button::Appearance>,
},
}
impl Default for Button {
fn default() -> Self {
Self::Primary
}
}
impl Button {
impl IcedButton {
#[allow(clippy::trivially_copy_pass_by_ref)]
#[allow(clippy::match_same_arms)]
fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent {
let cosmic = theme.cosmic();
match self {
Button::Primary => &cosmic.accent_button,
Button::Secondary => &theme.current_container().component,
Button::Positive => &cosmic.success_button,
Button::Destructive => &cosmic.destructive_button,
Button::Text => &cosmic.text_button,
Button::Link => &cosmic.accent_button,
Button::LinkActive => &cosmic.accent_button,
Button::Transparent => &TRANSPARENT_COMPONENT,
Button::Deactivated => &theme.current_container().component,
Button::Card => &theme.current_container().component,
Button::Custom { .. } => &TRANSPARENT_COMPONENT,
IcedButton::Primary => &cosmic.accent_button,
IcedButton::Secondary => &theme.current_container().component,
IcedButton::Positive => &cosmic.success_button,
IcedButton::Destructive => &cosmic.destructive_button,
IcedButton::Text => &cosmic.text_button,
IcedButton::Link => &cosmic.accent_button,
IcedButton::LinkActive => &cosmic.accent_button,
IcedButton::Transparent => &TRANSPARENT_COMPONENT,
IcedButton::Deactivated => &theme.current_container().component,
IcedButton::Card => &theme.current_container().component,
IcedButton::Custom { .. } => &TRANSPARENT_COMPONENT,
}
}
}
impl button::StyleSheet for Theme {
type Style = Button;
impl iced_button::StyleSheet for Theme {
type Style = IcedButton;
fn active(&self, style: &Self::Style) -> button::Appearance {
if let Button::Custom { active, .. } = style {
fn active(&self, style: &Self::Style) -> iced_button::Appearance {
if let IcedButton::Custom { active, .. } = style {
return active(self);
}
let corner_radii = &self.cosmic().corner_radii;
let component = style.cosmic(self);
button::Appearance {
iced_button::Appearance {
border_radius: match style {
Button::Link => corner_radii.radius_0.into(),
Button::Card => corner_radii.radius_xs.into(),
IcedButton::Link => corner_radii.radius_0.into(),
IcedButton::Card => corner_radii.radius_xs.into(),
_ => corner_radii.radius_xl.into(),
},
background: match style {
Button::Link | Button::Text => None,
Button::LinkActive => Some(Background::Color(component.divider.into())),
IcedButton::Link | IcedButton::Text => None,
IcedButton::LinkActive => Some(Background::Color(component.divider.into())),
_ => Some(Background::Color(component.base.into())),
},
text_color: match style {
Button::Link | Button::LinkActive => component.base.into(),
IcedButton::Link | IcedButton::LinkActive => component.base.into(),
_ => component.on.into(),
},
..button::Appearance::default()
..iced_button::Appearance::default()
}
}
fn hovered(&self, style: &Self::Style) -> button::Appearance {
if let Button::Custom { hover, .. } = style {
fn hovered(&self, style: &Self::Style) -> iced_button::Appearance {
if let IcedButton::Custom { hover, .. } = style {
return hover(self);
}
let active = self.active(style);
let component = style.cosmic(self);
button::Appearance {
iced_button::Appearance {
background: match style {
Button::Link => None,
Button::LinkActive => Some(Background::Color(component.divider.into())),
IcedButton::Link => None,
IcedButton::LinkActive => Some(Background::Color(component.divider.into())),
_ => Some(Background::Color(component.hover.into())),
},
..active
}
}
fn focused(&self, style: &Self::Style) -> button::Appearance {
if let Button::Custom { hover, .. } = style {
fn focused(&self, style: &Self::Style) -> iced_button::Appearance {
if let IcedButton::Custom { hover, .. } = style {
return hover(self);
}
let active = self.active(style);
let component = style.cosmic(self);
button::Appearance {
iced_button::Appearance {
background: match style {
Button::Link => None,
Button::LinkActive => Some(Background::Color(component.divider.into())),
IcedButton::Link => None,
IcedButton::LinkActive => Some(Background::Color(component.divider.into())),
_ => Some(Background::Color(component.hover.into())),
},
..active
}
}
fn disabled(&self, style: &Self::Style) -> button::Appearance {
fn disabled(&self, style: &Self::Style) -> iced_button::Appearance {
let active = self.active(style);
if matches!(style, Button::Card) {
if matches!(style, IcedButton::Card) {
return active;
}
button::Appearance {
iced_button::Appearance {
shadow_offset: iced_core::Vector::default(),
background: active.background.map(|background| match background {
Background::Color(color) => Background::Color(Color {
@ -563,6 +559,7 @@ impl container::StyleSheet for Theme {
let palette = self.cosmic();
container::Appearance {
icon_color: Some(Color::from(palette.background.on)),
text_color: Some(Color::from(palette.background.on)),
background: Some(iced::Background::Color(palette.background.base.into())),
border_radius: 2.0.into(),
@ -577,6 +574,7 @@ impl container::StyleSheet for Theme {
header_top.alpha = 0.8;
container::Appearance {
icon_color: Some(Color::from(palette.accent.base)),
text_color: Some(Color::from(palette.background.on)),
background: Some(iced::Background::Gradient(iced_core::Gradient::Linear(
Linear::new(Radians(3.0 * PI / 2.0))
@ -592,6 +590,7 @@ impl container::StyleSheet for Theme {
let palette = self.cosmic();
container::Appearance {
icon_color: Some(Color::from(palette.primary.on)),
text_color: Some(Color::from(palette.primary.on)),
background: Some(iced::Background::Color(palette.primary.base.into())),
border_radius: 2.0.into(),
@ -603,6 +602,7 @@ impl container::StyleSheet for Theme {
let palette = self.cosmic();
container::Appearance {
icon_color: Some(Color::from(palette.secondary.on)),
text_color: Some(Color::from(palette.secondary.on)),
background: Some(iced::Background::Color(palette.secondary.base.into())),
border_radius: 2.0.into(),
@ -615,6 +615,7 @@ impl container::StyleSheet for Theme {
match self.layer {
cosmic_theme::Layer::Background => container::Appearance {
icon_color: Some(Color::from(palette.background.component.on)),
text_color: Some(Color::from(palette.background.component.on)),
background: Some(iced::Background::Color(
palette.background.component.base.into(),
@ -624,6 +625,7 @@ impl container::StyleSheet for Theme {
border_color: Color::TRANSPARENT,
},
cosmic_theme::Layer::Primary => container::Appearance {
icon_color: Some(Color::from(palette.primary.component.on)),
text_color: Some(Color::from(palette.primary.component.on)),
background: Some(iced::Background::Color(
palette.primary.component.base.into(),
@ -633,6 +635,7 @@ impl container::StyleSheet for Theme {
border_color: Color::TRANSPARENT,
},
cosmic_theme::Layer::Secondary => container::Appearance {
icon_color: Some(Color::from(palette.secondary.component.on)),
text_color: Some(Color::from(palette.secondary.component.on)),
background: Some(iced::Background::Color(
palette.secondary.component.base.into(),
@ -1021,29 +1024,6 @@ pub enum Svg {
/// No filtering is applied
#[default]
Default,
/// Icon fill color will match text color
Symbolic,
/// Icon fill color will match accent color
SymbolicActive,
/// Icon fill color will match on primary color
SymbolicPrimary,
/// Icon fill color will use accent color
SymbolicLink,
}
impl Hash for Svg {
fn hash<H: Hasher>(&self, state: &mut H) {
let id = match self {
Svg::Custom(_) => 0,
Svg::Default => 1,
Svg::Symbolic => 2,
Svg::SymbolicActive => 3,
Svg::SymbolicPrimary => 4,
Svg::SymbolicLink => 5,
};
id.hash(state);
}
}
impl Svg {
@ -1060,18 +1040,6 @@ impl svg::StyleSheet for Theme {
match style {
Svg::Default => svg::Appearance::default(),
Svg::Custom(appearance) => appearance(self),
Svg::Symbolic => svg::Appearance {
color: Some(self.current_container().on.into()),
},
Svg::SymbolicActive => svg::Appearance {
color: Some(self.cosmic().accent.base.into()),
},
Svg::SymbolicPrimary => svg::Appearance {
color: Some(self.cosmic().accent.on.into()),
},
Svg::SymbolicLink => svg::Appearance {
color: Some(self.cosmic().accent.base.into()),
},
}
}
}

View file

@ -1,62 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{
theme::{self, THEME},
Element, Renderer,
};
use iced::widget;
/// A button widget with COSMIC styling
#[must_use]
pub const fn button<Message>(style: theme::Button) -> Button<Message> {
Button {
style,
message: None,
}
}
/// A button widget with COSMIC styling
pub struct Button<Message> {
style: theme::Button,
message: Option<Message>,
}
impl<Message: 'static> Button<Message> {
/// The message to emit on button press.
#[must_use]
pub fn on_press(mut self, message: Message) -> Self {
self.message = Some(message);
self
}
/// A button with an icon.
pub fn icon(
self,
style: theme::Svg,
icon: &str,
size: u16,
) -> widget::Button<Message, Renderer> {
self.custom(vec![super::icon(icon, size).style(style).into()])
}
/// A button with text.
pub fn text(self, text: &str) -> widget::Button<Message, Renderer> {
self.custom(vec![text.into()])
}
/// A custom button that has the desired default spacing and padding.
pub fn custom(self, children: Vec<Element<Message>>) -> widget::Button<Message, Renderer> {
let theme = THEME.with(|t| t.borrow().clone());
let theme = theme.cosmic();
let button = widget::button(widget::row(children).spacing(8))
.style(self.style)
.padding([theme.space_xxs(), theme.space_s()]);
if let Some(message) = self.message {
button.on_press(message)
} else {
button
}
}
}

View file

@ -0,0 +1,50 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::Builder;
use super::Style;
use crate::widget::icon::{self, Handle};
use iced_core::{font::Weight, widget::Id, Length, Padding};
use std::borrow::Cow;
pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>;
pub struct Hyperlink {
trailing_icon: bool,
}
pub fn hyperlink<'a, Message>() -> Button<'a, Message> {
Button::new(Hyperlink {
trailing_icon: false,
})
}
impl<'a, Message> Button<'a, Message> {
pub fn new(link: Hyperlink) -> Self {
Self {
id: Id::unique(),
label: Cow::Borrowed(""),
on_press: None,
width: Length::Shrink,
height: Length::Shrink,
padding: Padding::from(0),
spacing: 0,
icon_size: 16,
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Link,
variant: link,
}
}
pub const fn with_icon(mut self) -> Self {
self.variant.trailing_icon = true;
self
}
}
pub fn icon() -> Handle {
icon::handle::from_svg_bytes(&include_bytes!("../../../res/external-link.svg")[..])
.symbolic(true)
}

179
src/widget/button/icon.rs Normal file
View file

@ -0,0 +1,179 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{button, Builder, Style};
use crate::{widget::icon::Handle, Element};
use apply::Apply;
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
use std::borrow::Cow;
pub type Button<'a, Message> = Builder<'a, Message, Icon>;
pub struct Icon {
handle: Handle,
selected: bool,
vertical: bool,
}
pub fn icon<Message>(handle: impl Into<Handle>) -> Button<'static, Message> {
Button::new(Icon {
handle: handle.into(),
selected: false,
vertical: false,
})
}
impl<'a, Message> Button<'a, Message> {
pub fn new(icon: Icon) -> Self {
crate::theme::THEME.with(|theme_cell| {
let theme = theme_cell.borrow();
let theme = theme.cosmic();
let padding = theme.space_xxs();
Self {
id: Id::unique(),
label: Cow::Borrowed(""),
on_press: None,
width: Length::Shrink,
height: Length::Fixed(46.0),
padding: Padding::from(padding),
spacing: theme.space_xxxs(),
icon_size: 16,
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Icon,
variant: icon,
}
})
}
/// Applies the **Extra Small** button size preset.
pub fn extra_small(mut self) -> Self {
crate::theme::THEME.with(|theme_cell| {
let theme = theme_cell.borrow();
let theme = theme.cosmic();
self.font_size = 14;
self.font_weight = Weight::Normal;
self.icon_size = 16;
self.line_height = 20;
self.height = Length::Fixed(36.0);
self.padding = Padding::from(theme.space_xxs());
self.spacing = theme.space_xxxs();
});
self
}
/// Applies the **Medium** button size preset.
pub fn medium(mut self) -> Self {
crate::theme::THEME.with(|theme_cell| {
let theme = theme_cell.borrow();
let theme = theme.cosmic();
self.font_size = 24;
self.font_weight = Weight::Normal;
self.icon_size = 32;
self.line_height = 32;
self.height = Length::Fixed(56.0);
self.padding = Padding::from(theme.space_xs());
self.spacing = theme.space_xxs();
});
self
}
/// Applies the **Large** button size preset.
pub fn large(mut self) -> Self {
crate::theme::THEME.with(|theme_cell| {
let theme = theme_cell.borrow();
let theme = theme.cosmic();
self.font_size = 28;
self.font_weight = Weight::Light;
self.icon_size = 40;
self.line_height = 36;
self.height = Length::Fixed(64.0);
self.padding = Padding::from(theme.space_xs());
self.spacing = theme.space_xxs();
});
self
}
/// Applies the **Extra Large** button size preset.
pub fn extra_large(mut self) -> Self {
crate::theme::THEME.with(|theme_cell| {
let theme = theme_cell.borrow();
let theme = theme.cosmic();
let padding = theme.space_xs();
self.font_size = 32;
self.font_weight = Weight::Light;
self.icon_size = 56;
self.line_height = 44;
self.height = Length::Fixed(80.0);
self.padding = Padding::from(padding);
self.spacing = theme.space_xxs();
});
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.variant.selected = selected;
self
}
}
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> {
fn from(builder: Button<'a, Message>) -> Element<'a, Message> {
let mut content = Vec::with_capacity(2);
content.push(
crate::widget::icon(builder.variant.handle.clone())
.size(builder.icon_size)
.into(),
);
if !builder.label.is_empty() {
content.push(
crate::widget::text(builder.label)
.size(builder.font_size)
.line_height(LineHeight::Absolute(builder.line_height.into()))
.font({
let mut font = crate::font::DEFAULT;
font.weight = builder.font_weight;
font
})
.into(),
);
}
let button = if builder.variant.vertical {
crate::widget::column::with_children(content)
.padding(builder.padding)
.width(builder.width)
.height(builder.height)
.spacing(builder.spacing)
.align_items(Alignment::Center)
.apply(button)
} else {
crate::widget::row::with_children(content)
.padding(builder.padding)
.width(builder.width)
.height(builder.height)
.spacing(builder.spacing)
.align_items(Alignment::Center)
.apply(button)
};
button
.padding(0)
.id(builder.id)
.on_press_maybe(builder.on_press)
.style(builder.style)
.into()
}
}

113
src/widget/button/mod.rs Normal file
View file

@ -0,0 +1,113 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub mod hyperlink;
pub use hyperlink::Button as LinkButton;
mod icon;
pub use icon::icon;
pub use icon::Button as IconButton;
mod style;
pub use style::{Appearance, StyleSheet};
mod text;
pub use text::Button as TextButton;
pub use text::{destructive, standard, suggested, text};
mod widget;
pub use widget::{draw, focus, layout, mouse_interaction, Button};
pub use crate::theme::Button as Style;
use crate::Element;
use iced_core::font::Weight;
use iced_core::widget::Id;
use iced_core::{Length, Padding};
use std::borrow::Cow;
pub fn button<'a, Message>(
content: impl Into<Element<'a, Message>>,
) -> Button<'a, Message, crate::Renderer> {
Button::new(content)
}
#[must_use]
pub struct Builder<'a, Message, Variant> {
id: Id,
label: Cow<'a, str>,
// tooltip: Cow<'a, str>,
on_press: Option<Message>,
width: Length,
height: Length,
padding: Padding,
spacing: u16,
icon_size: u16,
line_height: u16,
font_size: u16,
font_weight: Weight,
style: Style,
variant: Variant,
}
// /// A [`Button`] with an icon, which may be used in place of text.
// pub const fn icon<'a>(selected: bool) -> Button<'a> {
// Builder::new(Standard::Icon { selected })
// }
impl<'a, Message, Variant> Builder<'a, Message, Variant> {
/// Sets the [`Id`] of the [`Button`].
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
self.label = label.into();
self
}
/// Sets the width of the [`Button`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Button`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the [`Padding`] of the [`Button`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(on_press);
self
}
/// Sets the message that will be produced when the [`Button`] is pressed,
/// if `Some`.
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn tooltip(mut self, label: impl Into<Cow<'a, str>>) -> Self {
self.label = label.into();
self
}
}

View file

@ -0,0 +1,90 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Change the apperance of a button.
use iced_core::{Background, BorderRadius, Color, Vector};
/// The appearance of a button.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The amount of offset to apply to the shadow of the button.
pub shadow_offset: Vector,
/// The [`Background`] of the button.
pub background: Option<Background>,
/// The border radius of the button.
pub border_radius: BorderRadius,
/// The border width of the button.
pub border_width: f32,
/// The border [`Color`] of the button.
pub border_color: Color,
/// Opacity of the button.
pub opacity: f32,
/// An outline placed around the border.
pub outline_width: f32,
/// The [`Color`] of the outline.
pub outline_color: Color,
/// The icon [`Color`] of the button.
pub icon_color: Option<Color>,
/// The text [`Color`] of the button.
pub text_color: Color,
}
impl Appearance {
// TODO: `BorderRadius` is not `const fn` compatible.
pub fn new() -> Self {
Self {
shadow_offset: Vector::new(0.0, 0.0),
background: None,
border_radius: BorderRadius::from(0.0),
border_width: 0.0,
border_color: Color::TRANSPARENT,
opacity: 1.0,
outline_width: 0.0,
outline_color: Color::TRANSPARENT,
icon_color: None,
text_color: Color::BLACK,
}
}
}
impl std::default::Default for Appearance {
fn default() -> Self {
Self::new()
}
}
/// A set of rules that dictate the style of a button.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the active [`Appearance`] of a button.
fn active(&self, focused: bool, style: &Self::Style) -> Appearance;
/// Produces the disabled [`Appearance`] of a button.
fn disabled(&self, style: &Self::Style) -> Appearance;
/// [`Appearance`] when the button is the target of a DND operation.
fn drop_target(&self, style: &Self::Style) -> Appearance {
self.hovered(false, style)
}
/// Produces the hovered [`Appearance`] of a button.
fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance;
/// Produces the pressed [`Appearance`] of a button.
fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance;
/// [`Appearance`] when a button is selected.
fn selected(&self, focused: bool, style: &Self::Style) -> Appearance;
}

123
src/widget/button/text.rs Normal file
View file

@ -0,0 +1,123 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{button, Builder, Style};
use crate::widget::{self, icon::Handle, row};
use crate::{ext::CollectionWidget, Element};
use apply::Apply;
use iced_core::{font::Weight, text::LineHeight, widget::Id, Alignment, Length, Padding};
use std::borrow::Cow;
/// A [`Button`] with the highest level of attention.
///
/// There should only be one primary button used per page.
pub type Button<'a, Message> = Builder<'a, Message, Text>;
pub fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new())
.label(label)
.style(Style::Destructive)
}
pub fn suggested<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new())
.label(label)
.style(Style::Suggested)
}
pub fn standard<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()).label(label)
}
pub fn text<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()).label(label).style(Style::Text)
}
pub struct Text {
pub(super) leading_icon: Option<crate::widget::icon::Handle>,
pub(super) trailing_icon: Option<crate::widget::icon::Handle>,
}
impl Text {
pub const fn new() -> Self {
Self {
leading_icon: None,
trailing_icon: None,
}
}
}
impl<'a, Message> Button<'a, Message> {
pub fn new(text: Text) -> Self {
crate::theme::THEME.with(|theme_cell| {
let theme = theme_cell.borrow();
let theme = theme.cosmic();
Self {
id: Id::unique(),
label: Cow::Borrowed(""),
on_press: None,
width: Length::Shrink,
height: Length::Fixed(theme.space_l().into()),
padding: Padding::from([0, theme.space_s()]),
spacing: theme.space_xxxs(),
icon_size: 16,
line_height: 20,
font_size: 14,
font_weight: Weight::Normal,
style: Style::Standard,
variant: text,
}
})
}
pub fn leading_icon(mut self, icon: impl Into<Handle>) -> Self {
self.variant.leading_icon = Some(icon.into());
self
}
pub fn trailing_icon(mut self, icon: impl Into<Handle>) -> Self {
self.variant.trailing_icon = Some(icon.into());
self
}
}
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> {
fn from(mut b: Button<'a, Message>) -> Element<'a, Message> {
// TODO: Determine why this needs to be set before the label to prevent lifetime conflict.
let trailing_icon = b
.variant
.trailing_icon
.map(|i| Element::from(widget::icon(i).size(b.icon_size)));
row::with_capacity(3)
// Optional icon to place before label.
.push_maybe(
b.variant
.leading_icon
.map(|i| widget::icon(i).size(b.icon_size)),
)
// Optional label between icons.
.push_maybe((!b.label.is_empty()).then(|| {
let mut font = crate::font::DEFAULT;
font.weight = b.font_weight;
crate::widget::text(b.label)
.size(b.font_size)
.line_height(LineHeight::Absolute(b.line_height.into()))
.font(font)
}))
// Optional icon to place behind the label.
.push_maybe(trailing_icon)
.padding(b.padding)
.width(b.width)
.height(b.height)
.spacing(b.spacing)
.align_items(Alignment::Center)
.apply(button)
.padding(0)
.id(b.id)
.on_press_maybe(b.on_press.take())
.style(b.style)
.into()
}
}

677
src/widget/button/widget.rs Normal file
View file

@ -0,0 +1,677 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
use iced_runtime::core::widget::Id;
use iced_runtime::{keyboard, Command};
use iced_core::event::{self, Event};
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::touch;
use iced_core::widget::tree::{self, Tree};
use iced_core::widget::Operation;
use iced_core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell,
Vector, Widget,
};
use iced_renderer::core::widget::{operation, OperationOutputWrapper};
pub use super::style::{Appearance, StyleSheet};
/// A generic widget that produces a message when pressed.
///
/// ```no_run
/// # type Button<'a, Message> =
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
///
/// let button = Button::new("Press me!").on_press(Message::ButtonPressed);
/// ```
///
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
/// be disabled:
///
/// ```
/// # type Button<'a, Message> =
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
///
/// fn disabled_button<'a>() -> Button<'a, Message> {
/// Button::new("I'm disabled!")
/// }
///
/// fn enabled_button<'a>() -> Button<'a, Message> {
/// disabled_button().on_press(Message::ButtonPressed)
/// }
/// ```
#[allow(missing_debug_implementations)]
#[must_use]
pub struct Button<'a, Message, Renderer>
where
Renderer: iced_core::Renderer,
Renderer::Theme: StyleSheet,
{
id: Id,
#[cfg(feature = "a11y")]
name: Option<Cow<'a, str>>,
#[cfg(feature = "a11y")]
description: Option<iced_accessibility::Description<'a>>,
#[cfg(feature = "a11y")]
label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
content: Element<'a, Message, Renderer>,
on_press: Option<Message>,
width: Length,
height: Length,
padding: Padding,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
Renderer: iced_core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a new [`Button`] with the given content.
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
Button {
id: Id::unique(),
#[cfg(feature = "a11y")]
name: None,
#[cfg(feature = "a11y")]
description: None,
#[cfg(feature = "a11y")]
label: None,
content: content.into(),
on_press: None,
width: Length::Shrink,
height: Length::Shrink,
padding: Padding::new(5.0),
style: <Renderer::Theme as StyleSheet>::Style::default(),
}
}
/// Sets the width of the [`Button`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Button`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the [`Padding`] of the [`Button`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(on_press);
self
}
/// Sets the message that will be produced when the [`Button`] is pressed,
/// if `Some`.
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press;
self
}
/// Sets the style variant of this [`Button`].
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self {
self.style = style;
self
}
/// Sets the [`Id`] of the [`Button`].
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
#[cfg(feature = "a11y")]
/// Sets the name of the [`Button`].
pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
self.name = Some(name.into());
self
}
#[cfg(feature = "a11y")]
/// Sets the description of the [`Button`].
pub fn description_widget<T: iced_accessibility::Describes>(mut self, description: &T) -> Self {
self.description = Some(iced_accessibility::Description::Id(
description.description(),
));
self
}
#[cfg(feature = "a11y")]
/// Sets the description of the [`Button`].
pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
self.description = Some(iced_accessibility::Description::Text(description.into()));
self
}
#[cfg(feature = "a11y")]
/// Sets the label of the [`Button`].
pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
self.label = Some(label.label().into_iter().map(|l| l.into()).collect());
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + iced_core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(std::slice::from_mut(&mut self.content));
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.height,
self.padding,
|renderer, limits| self.content.as_widget().layout(renderer, limits),
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
});
let state = tree.state.downcast_mut::<State>();
operation.focusable(state, Some(&self.id));
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
if let event::Status::Captured = self.content.as_widget_mut().on_event(
&mut tree.children[0],
event.clone(),
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
) {
return event::Status::Captured;
}
update(
self.id.clone(),
event,
layout,
cursor,
shell,
&self.on_press,
|| tree.state.downcast_mut::<State>(),
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let styling = draw(
renderer,
bounds,
cursor,
self.on_press.is_some(),
theme,
&self.style,
|| tree.state.downcast_ref::<State>(),
);
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
&renderer::Style {
icon_color: styling.icon_color.unwrap_or(renderer_style.icon_color),
text_color: styling.text_color,
scale_factor: renderer_style.scale_factor,
},
content_layout,
cursor,
&bounds,
);
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(layout, cursor, self.on_press.is_some())
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
)
}
#[cfg(feature = "a11y")]
/// get the a11y nodes for the widget
fn a11y_nodes(
&self,
layout: Layout<'_>,
state: &Tree,
p: mouse::Cursor,
) -> iced_accessibility::A11yTree {
use iced_accessibility::{
accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role},
A11yNode, A11yTree,
};
let child_layout = layout.children().next().unwrap();
let child_tree = &state.children[0];
let child_tree = self
.content
.as_widget()
.a11y_nodes(child_layout, &child_tree, p);
let Rectangle {
x,
y,
width,
height,
} = layout.bounds();
let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64);
let is_hovered = state.state.downcast_ref::<State>().is_hovered;
let mut node = NodeBuilder::new(Role::Button);
node.add_action(Action::Focus);
node.add_action(Action::Default);
node.set_bounds(bounds);
if let Some(name) = self.name.as_ref() {
node.set_name(name.clone());
}
match self.description.as_ref() {
Some(iced_accessibility::Description::Id(id)) => {
node.set_described_by(
id.iter()
.cloned()
.map(|id| NodeId::from(id))
.collect::<Vec<_>>(),
);
}
Some(iced_accessibility::Description::Text(text)) => {
node.set_description(text.clone());
}
None => {}
}
if let Some(label) = self.label.as_ref() {
node.set_labelled_by(label.clone());
}
if self.on_press.is_none() {
node.set_disabled()
}
if is_hovered {
node.set_hovered()
}
node.set_default_action_verb(DefaultActionVerb::Click);
A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree)
}
fn id(&self) -> Option<Id> {
Some(self.id.clone())
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
}
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer>
where
Message: Clone + 'a,
Renderer: iced_core::Renderer + 'a,
Renderer::Theme: StyleSheet,
{
fn from(button: Button<'a, Message, Renderer>) -> Self {
Self::new(button)
}
}
/// The local state of a [`Button`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_hovered: bool,
is_pressed: bool,
is_focused: bool,
}
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State::default()
}
/// Returns whether the [`Button`] is currently focused or not.
pub fn is_focused(self) -> bool {
self.is_focused
}
/// Returns whether the [`Button`] is currently hovered or not.
pub fn is_hovered(self) -> bool {
self.is_hovered
}
/// Focuses the [`Button`].
pub fn focus(&mut self) {
self.is_focused = true;
}
/// Unfocuses the [`Button`].
pub fn unfocus(&mut self) {
self.is_focused = false;
}
}
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
/// accordingly.
#[allow(clippy::needless_pass_by_value)]
pub fn update<'a, Message: Clone>(
_id: Id,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
on_press: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if on_press.is_some() {
let bounds = layout.bounds();
if cursor.is_over(bounds) {
let state = state();
state.is_pressed = true;
return event::Status::Captured;
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = on_press.clone() {
let state = state();
if state.is_pressed {
state.is_pressed = false;
let bounds = layout.bounds();
if cursor.is_over(bounds) {
shell.publish(on_press);
}
return event::Status::Captured;
}
}
}
#[cfg(feature = "a11y")]
Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => {
let state = state();
if let Some(Some(on_press)) = (id == event_id
&& matches!(action, iced_accessibility::accesskit::Action::Default))
.then(|| on_press.clone())
{
state.is_pressed = false;
shell.publish(on_press);
}
return event::Status::Captured;
}
Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => {
if let Some(on_press) = on_press.clone() {
let state = state();
if state.is_focused && key_code == keyboard::KeyCode::Enter {
state.is_pressed = true;
shell.publish(on_press);
return event::Status::Captured;
}
}
}
Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
let state = state();
state.is_hovered = false;
state.is_pressed = false;
}
_ => {}
}
event::Status::Ignored
}
/// Draws a [`Button`].
pub fn draw<'a, Renderer: iced_core::Renderer>(
renderer: &mut Renderer,
bounds: Rectangle,
cursor: mouse::Cursor,
is_enabled: bool,
style_sheet: &dyn StyleSheet<Style = <Renderer::Theme as StyleSheet>::Style>,
style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State,
) -> Appearance
where
Renderer::Theme: StyleSheet,
{
let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
let state: &State = state();
let styling = if !is_enabled {
style_sheet.disabled(style)
} else if is_mouse_over {
if state.is_pressed {
style_sheet.pressed(state.is_focused, style)
} else {
style_sheet.hovered(state.is_focused, style)
}
} else {
style_sheet.active(state.is_focused, style)
};
let doubled_border_width = styling.border_width * 2.0;
let doubled_outline_width = styling.outline_width * 2.0;
if styling.outline_width > 0.0 {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x - styling.border_width - styling.outline_width,
y: bounds.y - styling.border_width - styling.outline_width,
width: bounds.width + doubled_border_width + doubled_outline_width,
height: bounds.height + doubled_border_width + doubled_outline_width,
},
border_radius: styling.border_radius,
border_width: styling.outline_width,
border_color: styling.outline_color,
},
Color::TRANSPARENT,
);
}
if styling.background.is_some() || styling.border_width > 0.0 {
if styling.shadow_offset != Vector::default() {
// TODO: Implement proper shadow support
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + styling.shadow_offset.x,
y: bounds.y + styling.shadow_offset.y,
..bounds
},
border_radius: styling.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Background::Color([0.0, 0.0, 0.0, 0.5].into()),
);
}
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: styling.border_radius,
border_width: styling.border_width,
border_color: styling.border_color,
},
styling
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
styling
}
/// Computes the layout of a [`Button`].
pub fn layout<Renderer>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
height: Length,
padding: Padding,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
let mut content = layout_content(renderer, &limits.pad(padding));
let padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size()).pad(padding);
content.move_to(Point::new(padding.left, padding.top));
layout::Node::with_children(size, vec![content])
}
/// Returns the [`mouse::Interaction`] of a [`Button`].
#[must_use]
pub fn mouse_interaction(
layout: Layout<'_>,
cursor: mouse::Cursor,
is_enabled: bool,
) -> mouse::Interaction {
let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over && is_enabled {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id))
}
impl operation::Focusable for State {
fn is_focused(&self) -> bool {
State::is_focused(*self)
}
fn focus(&mut self) {
State::focus(self);
}
fn unfocus(&mut self) {
State::unfocus(self);
}
}

View file

@ -1 +1,4 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub mod style;

View file

@ -1,3 +1,6 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced_core::{Background, Color};
/// Appearance of the cards.

View file

@ -1,17 +1,18 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::cell::RefCell;
use crate::ext::CollectionWidget;
use crate::widget::{column, row};
use crate::Element;
use apply::Apply;
use derive_setters::Setters;
use iced::widget::{column, row};
use iced_core::{alignment, Length, Size};
use std::cell::RefCell;
/// Responsively generates rows and columns of widgets based on its dimmensions.
#[derive(Setters)]
pub struct FlexRow<'a, Message> {
#[allow(clippy::type_complexity)]
#[setters(skip)]
generator: Box<dyn Fn(&mut Vec<Element<'a, Message>>, Size) -> u16 + 'a>,
/// Sets the space between each column of items.
@ -103,17 +104,15 @@ impl<'a, Message: 'static> From<FlexRow<'a, Message>> for Element<'a, Message> {
let mut iterator = elements.drain(..);
while let Some(element) = iterator.next() {
let mut elements_row = Vec::with_capacity(items_per_row);
elements_row.push(element);
let elements_row = row::with_capacity(items_per_row)
.spacing(container.row_spacing)
.push(element)
.extend(iterator.by_ref().take(items_per_row - 1));
for element in iterator.by_ref().take(items_per_row - 1) {
elements_row.push(element);
}
elements_column.push(row(elements_row).spacing(container.row_spacing).into());
elements_column.push(elements_row.into());
}
column(elements_column)
column::with_children(elements_column)
.spacing(container.column_spacing)
.apply(iced::widget::container)
.align_x(container.align_x)

View file

@ -1,10 +1,10 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{theme, Element};
use crate::{ext::CollectionWidget, widget, Element};
use apply::Apply;
use derive_setters::Setters;
use iced::{self, widget, Length};
use iced::Length;
use std::borrow::Cow;
#[must_use]
@ -89,51 +89,43 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
/// Converts the headerbar builder into an Iced element.
pub fn into_element(mut self) -> Element<'a, Message> {
let mut packed: Vec<Element<Message>> = Vec::with_capacity(4);
// Take ownership of the regions to be packed.
let start = std::mem::take(&mut self.start);
let center = std::mem::take(&mut self.center);
let mut end = std::mem::take(&mut self.end);
// If elements exist in the start region, append them here.
if !start.is_empty() {
packed.push(
iced::widget::row(start)
.align_items(iced::Alignment::Center)
.apply(iced::widget::container)
.align_x(iced::alignment::Horizontal::Left)
.into(),
);
}
// If elements exist in the center region, use them here.
// This will otherwise use the title as a widget if a title was defined.
packed.push(if !center.is_empty() {
iced::widget::row(center)
.align_items(iced::Alignment::Center)
.apply(iced::widget::container)
.align_x(iced::alignment::Horizontal::Center)
.into()
} else if self.title.is_empty() {
widget::horizontal_space(Length::Fill).into()
} else {
self.title_widget()
});
// Also packs the window controls at the very end.
end.push(iced::widget::horizontal_space(Length::Fixed(12.0)).into());
end.push(widget::horizontal_space(Length::Fixed(12.0)).into());
end.push(self.window_controls());
packed.push(
iced::widget::row(end)
.align_items(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Right)
.into(),
);
// Creates the headerbar widget.
let mut widget = widget::row(packed)
let mut widget = widget::row::with_capacity(4)
// If elements exist in the start region, append them here.
.push_maybe((!start.is_empty()).then(|| {
widget::row::with_children(start)
.align_items(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Left)
}))
// If elements exist in the center region, use them here.
// This will otherwise use the title as a widget if a title was defined.
.push(if !center.is_empty() {
widget::row::with_children(center)
.align_items(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Center)
.into()
} else if self.title.is_empty() {
widget::horizontal_space(Length::Fill).into()
} else {
self.title_widget()
})
.push(
widget::row::with_children(end)
.align_items(iced::Alignment::Center)
.apply(widget::container)
.align_x(iced::alignment::Horizontal::Right),
)
.height(Length::Fixed(50.0))
.padding(8)
.spacing(8)
@ -159,7 +151,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
let mut title = Cow::default();
std::mem::swap(&mut title, &mut self.title);
super::text(title)
widget::text(title)
.size(16)
.font(crate::font::FONT_SEMIBOLD)
.apply(widget::container)
@ -172,30 +164,30 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
/// Creates the widget for window controls.
fn window_controls(&mut self) -> Element<'a, Message> {
let mut widgets: Vec<Element<_>> = Vec::with_capacity(3);
let icon = |name, size, on_press| {
super::icon(name, size)
.force_svg(true)
.style(crate::theme::Svg::SymbolicActive)
.apply(widget::button)
.style(theme::Button::Text)
widget::icon::handle::from_name(name)
.size(size)
.handle()
.apply(widget::button::icon)
.on_press(on_press)
};
if let Some(message) = self.on_minimize.take() {
widgets.push(icon("window-minimize-symbolic", 16, message).into());
}
if let Some(message) = self.on_maximize.take() {
widgets.push(icon("window-maximize-symbolic", 16, message).into());
}
if let Some(message) = self.on_close.take() {
widgets.push(icon("window-close-symbolic", 16, message).into());
}
widget::row(widgets)
widget::row::with_capacity(3)
.push_maybe(
self.on_minimize
.take()
.map(|m| icon("window-minimize-symbolic", 16, m)),
)
.push_maybe(
self.on_maximize
.take()
.map(|m| icon("window-maximize-symbolic", 16, m)),
)
.push_maybe(
self.on_close
.take()
.map(|m| icon("window-close-symbolic", 16, m)),
)
.spacing(8)
.apply(widget::container)
.height(Length::Fill)

View file

@ -1,311 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Lazily-generated SVG icon widget for Iced.
use crate::{Element, Renderer};
use derive_setters::Setters;
use iced::{
widget::{image, svg, Image},
ContentFit, Length,
};
use std::{
borrow::Cow, collections::hash_map::DefaultHasher, ffi::OsStr, hash::Hash, hash::Hasher,
path::Path, path::PathBuf,
};
use tracing::error;
#[derive(Clone, Debug, Hash)]
pub enum Handle {
Image(image::Handle),
Svg(svg::Handle),
}
#[derive(Clone, Debug, Hash)]
pub enum IconSource<'a> {
Path(Cow<'a, Path>),
Name(Cow<'a, str>),
Handle(Handle),
}
impl<'a> IconSource<'a> {
/// Loads the icon as either an image or svg [`Handle`].
#[must_use]
pub fn load(
&self,
size: u16,
theme: Option<&str>,
svg: bool,
default_fallbacks: bool,
) -> Handle {
let mut name_path_buffer: Option<PathBuf>;
let icon: Option<&Path> = match self {
IconSource::Handle(handle) => return handle.clone(),
IconSource::Path(ref path) => Some(path),
#[cfg(unix)]
IconSource::Name(ref name) => {
name_path_buffer = None;
if let Some(path) = load_icon(name, size, theme) {
name_path_buffer = Some(path);
} else if default_fallbacks {
for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) {
if let Some(path) = load_icon(name, size, theme) {
name_path_buffer = Some(path);
break;
}
}
}
name_path_buffer.as_deref()
}
// TODO: Icon loading mechanism for non-Unix systems
#[cfg(not(unix))]
IconSource::Name(_) => None,
};
let is_svg = svg
|| icon
.as_ref()
.map_or(true, |path| path.extension() == Some(OsStr::new("svg")));
if is_svg {
let handle = if let Some(path) = icon {
svg::Handle::from_path(path)
} else {
error!("svg icon '{self:?}' size {size} not found");
svg::Handle::from_memory(Vec::new())
};
Handle::Svg(handle)
} else if let Some(icon) = icon {
Handle::Image(icon.into())
} else {
error!("icon '{self:?}' size {size} not found");
Handle::Image(image::Handle::from_memory(Vec::new()))
}
}
/// Get a handle to a raster image from a path.
pub fn raster_from_path(path: impl Into<PathBuf>) -> Self {
IconSource::Handle(Handle::Image(image::Handle::from_path(path)))
}
/// Get a handle to a raster image from memory.
pub fn raster_from_memory(
bytes: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Self {
IconSource::Handle(Handle::Image(image::Handle::from_memory(bytes)))
}
/// Get a handle to a raster image from RGBA data, where you must define the width and height.
pub fn raster_from_pixels(
width: u32,
height: u32,
pixels: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Self {
IconSource::Handle(Handle::Image(image::Handle::from_pixels(
width, height, pixels,
)))
}
/// Get a handle to a SVG from a path.
pub fn svg_from_path(path: impl Into<PathBuf>) -> Self {
IconSource::Handle(Handle::Svg(svg::Handle::from_path(path)))
}
/// Get a handle to a SVG from memory.
pub fn svg_from_memory(bytes: impl Into<Cow<'static, [u8]>>) -> Self {
IconSource::Handle(Handle::Svg(svg::Handle::from_memory(bytes)))
}
}
impl<'a> From<Cow<'a, Path>> for IconSource<'a> {
fn from(value: Cow<'a, Path>) -> Self {
Self::Path(value)
}
}
impl From<PathBuf> for IconSource<'static> {
fn from(value: PathBuf) -> Self {
Self::Path(Cow::Owned(value))
}
}
impl<'a> From<&'a Path> for IconSource<'a> {
fn from(value: &'a Path) -> Self {
Self::Path(Cow::Borrowed(value))
}
}
impl<'a> From<Cow<'a, str>> for IconSource<'a> {
fn from(value: Cow<'a, str>) -> Self {
Self::Name(value)
}
}
impl From<String> for IconSource<'static> {
fn from(value: String) -> Self {
Self::Name(value.into())
}
}
impl<'a> From<&'a str> for IconSource<'a> {
fn from(value: &'a str) -> Self {
Self::Name(value.into())
}
}
impl From<image::Handle> for IconSource<'static> {
fn from(handle: image::Handle) -> Self {
Self::Handle(Handle::Image(handle))
}
}
impl From<svg::Handle> for IconSource<'static> {
fn from(handle: svg::Handle) -> Self {
Self::Handle(Handle::Svg(handle))
}
}
/// A lazily-generated icon.
#[derive(Setters)]
pub struct Icon<'a> {
#[setters(skip)]
source: IconSource<'a>,
#[setters(strip_option, into)]
theme: Option<Cow<'a, str>>,
style: crate::theme::Svg,
size: u16,
content_fit: ContentFit,
#[setters(strip_option)]
width: Option<Length>,
#[setters(strip_option)]
height: Option<Length>,
force_svg: bool,
default_fallbacks: bool,
}
// XXX Hopefully this will be enough precision
impl Hash for Icon<'_> {
#[allow(clippy::cast_possible_truncation)]
fn hash<H: Hasher>(&self, state: &mut H) {
self.source.hash(state);
self.theme.hash(state);
self.style.hash(state);
self.size.hash(state);
self.content_fit.hash(state);
self.force_svg.hash(state);
match self.width {
Some(Length::Fill) => 0.hash(state),
Some(Length::Shrink) => 1.hash(state),
Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state),
Some(Length::FillPortion(p)) => p.hash(state),
None => 2.hash(state),
}
match self.height {
Some(Length::Fill) => 0.hash(state),
Some(Length::Shrink) => 1.hash(state),
Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state),
Some(Length::FillPortion(p)) => p.hash(state),
None => 2.hash(state),
}
}
}
/// A lazily-generated icon.
#[must_use]
pub fn icon<'a>(source: impl Into<IconSource<'a>>, size: u16) -> Icon<'a> {
Icon {
content_fit: ContentFit::Fill,
height: None,
source: source.into(),
size,
style: crate::theme::Svg::default(),
theme: None,
width: None,
force_svg: false,
default_fallbacks: true,
}
}
impl<'a> Icon<'a> {
fn raster_element<Message: 'static>(&self, handle: image::Handle) -> Element<'static, Message> {
Image::new(handle)
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
.content_fit(self.content_fit)
.into()
}
fn svg_element<Message: 'static>(&self, handle: svg::Handle) -> Element<'static, Message> {
svg::Svg::<Renderer>::new(handle)
.style(self.style.clone())
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
.content_fit(self.content_fit)
.into()
}
#[must_use]
fn into_element<Message: 'static>(mut self) -> Element<'a, Message> {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
if self.theme.is_none() {
crate::icon_theme::DEFAULT.with(|f| f.borrow().hash(&mut hasher));
}
let hash = hasher.finish();
let mut source = IconSource::Name(Cow::Borrowed(""));
std::mem::swap(&mut source, &mut self.source);
iced::widget::lazy(hash, move |_| -> Element<Message> {
match source.load(
self.size,
self.theme.as_deref(),
self.force_svg,
self.default_fallbacks,
) {
Handle::Svg(handle) => self.svg_element(handle),
Handle::Image(handle) => self.raster_element(handle),
}
})
.into()
}
}
impl<'a, Message: 'static> From<Icon<'a>> for Element<'a, Message> {
fn from(icon: Icon<'a>) -> Self {
icon.into_element::<Message>()
}
}
#[must_use]
pub fn load_icon(name: &str, size: u16, theme: Option<&str>) -> Option<PathBuf> {
let icon = crate::icon_theme::DEFAULT.with(|default_theme| {
let default_theme = default_theme.borrow();
freedesktop_icons::lookup(name)
.with_size(size)
.with_theme(theme.unwrap_or(&default_theme))
.with_cache()
.find()
});
if icon.is_none() {
freedesktop_icons::lookup(name)
.with_size(size)
.with_cache()
.find()
} else {
icon
}
}

114
src/widget/icon/builder.rs Normal file
View file

@ -0,0 +1,114 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{handle, Handle, Icon};
use std::path::PathBuf;
#[must_use]
#[derive(derive_setters::Setters)]
pub struct Builder<'a> {
/// Name of icon to locate in an XDG icon path.
name: &'a str,
/// Checks for a fallback if the icon was not found.
fallback: bool,
/// Restrict the lookup to a given scale.
#[setters(strip_option)]
scale: Option<u16>,
/// Restrict the lookup to a given size.
#[setters(strip_option)]
size: Option<u16>,
/// Prioritizes SVG over PNG
prefer_svg: bool,
}
impl<'a> Builder<'a> {
pub const fn new(name: &'a str) -> Self {
Self {
name,
fallback: true,
size: None,
scale: None,
prefer_svg: false,
}
}
#[must_use]
pub fn path(mut self) -> Option<PathBuf> {
crate::icon_theme::DEFAULT.with(|theme| {
let theme = theme.borrow();
let locate = || {
let mut lookup = freedesktop_icons::lookup(self.name)
.with_theme(theme.as_ref())
.with_cache();
if let Some(scale) = self.scale {
lookup = lookup.with_scale(scale);
}
if let Some(size) = self.size {
lookup = lookup.with_size(size);
}
if self.prefer_svg {
lookup = lookup.force_svg();
}
lookup.find()
};
let mut result = locate();
// On failure, attempt to locate fallback icon.
if result.is_none() && self.fallback {
let name = std::mem::take(&mut self.name);
for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) {
self.name = name;
result = locate();
if result.is_some() {
break;
}
}
}
result
})
}
pub fn handle(self) -> Handle {
if let Some(path) = self.path() {
handle::from_path(path)
} else {
let bytes: &'static [u8] = &[];
handle::from_svg_bytes(bytes)
}
}
pub fn icon(self) -> Icon {
let size = self.size;
let icon = super::icon(self.handle());
match size {
Some(size) => icon.size(size),
None => icon,
}
}
}
impl<'a> From<Builder<'a>> for Handle {
fn from(builder: Builder<'a>) -> Self {
builder.handle()
}
}
impl<'a> From<Builder<'a>> for Icon {
fn from(builder: Builder<'a>) -> Self {
builder.icon()
}
}

88
src/widget/icon/handle.rs Normal file
View file

@ -0,0 +1,88 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use super::{Builder, Icon};
use crate::widget::{image, svg};
use std::borrow::Cow;
use std::ffi::OsStr;
use std::hash::Hash;
use std::path::PathBuf;
#[must_use]
#[derive(Clone, Debug, Hash, derive_setters::Setters)]
pub struct Handle {
pub symbolic: bool,
#[setters(skip)]
pub variant: Variant,
}
impl Handle {
pub fn icon(self) -> Icon {
super::icon(self)
}
}
#[must_use]
#[derive(Clone, Debug, Hash)]
pub enum Variant {
Image(image::Handle),
Svg(svg::Handle),
}
/// Create an icon handle from its XDG icon name.
pub fn from_name(name: &str) -> Builder {
Builder::new(name)
}
/// Create an icon handle from its path.
pub fn from_path(path: PathBuf) -> Handle {
Handle {
symbolic: path
.file_stem()
.and_then(OsStr::to_str)
.is_some_and(|name| name.ends_with("-symbolic")),
variant: if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
Variant::Svg(svg::Handle::from_path(path))
} else {
Variant::Image(image::Handle::from_path(path))
},
}
}
/// Create an image handle from memory.
pub fn from_raster_bytes(
bytes: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Handle {
Handle {
symbolic: false,
variant: Variant::Image(image::Handle::from_memory(bytes)),
}
}
/// Create an image handle from RGBA data, where you must define the width and height.
pub fn from_raster_pixels(
width: u32,
height: u32,
pixels: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Handle {
Handle {
symbolic: false,
variant: Variant::Image(image::Handle::from_pixels(width, height, pixels)),
}
}
/// Create a SVG handle from memory.
pub fn from_svg_bytes(bytes: impl Into<Cow<'static, [u8]>>) -> Handle {
Handle {
symbolic: false,
variant: Variant::Svg(svg::Handle::from_memory(bytes)),
}
}

104
src/widget/icon/mod.rs Normal file
View file

@ -0,0 +1,104 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Lazily-generated SVG icon widget for Iced.
mod builder;
pub use builder::Builder;
pub mod handle;
pub use handle::Handle;
use crate::{Element, Renderer};
use derive_setters::Setters;
use iced::widget::{Image, Svg};
use iced::{ContentFit, Length};
use std::borrow::Cow;
use std::path::PathBuf;
/// Create an [`Icon`] from a pre-existing [`Handle`]
pub fn icon(handle: Handle) -> Icon {
Icon {
content_fit: ContentFit::Fill,
handle,
height: None,
size: 16,
style: crate::theme::Svg::default(),
width: None,
}
}
/// Create an [`Icon`] from its path.
pub fn from_path(path: PathBuf) -> Icon {
icon(handle::from_path(path))
}
/// Create an image [`Icon`] from memory.
pub fn from_raster_bytes(
bytes: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Icon {
icon(handle::from_raster_bytes(bytes))
}
/// Create an image [`Icon`] from RGBA data, where you must define the width and height.
pub fn from_raster_pixels(
width: u32,
height: u32,
pixels: impl Into<Cow<'static, [u8]>>
+ std::convert::AsRef<[u8]>
+ std::marker::Send
+ std::marker::Sync
+ 'static,
) -> Icon {
icon(handle::from_raster_pixels(width, height, pixels))
}
/// Create a SVG [`Icon`] from memory.
pub fn from_svg_bytes(bytes: impl Into<Cow<'static, [u8]>>) -> Icon {
icon(handle::from_svg_bytes(bytes))
}
/// An image which may be an SVG or PNG.
#[must_use]
#[derive(Clone, Setters)]
pub struct Icon {
#[setters(skip)]
handle: Handle,
style: crate::theme::Svg,
pub(super) size: u16,
content_fit: ContentFit,
#[setters(strip_option)]
width: Option<Length>,
#[setters(strip_option)]
height: Option<Length>,
}
impl Icon {
#[must_use]
fn into_element<Message: 'static>(self) -> Element<'static, Message> {
match self.handle.variant {
handle::Variant::Image(handle) => Image::new(handle)
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
.content_fit(self.content_fit)
.into(),
handle::Variant::Svg(handle) => Svg::<Renderer>::new(handle)
.style(self.style.clone())
.width(self.width.unwrap_or(Length::Fixed(f32::from(self.size))))
.height(self.height.unwrap_or(Length::Fixed(f32::from(self.size))))
.content_fit(self.content_fit)
.symbolic(self.handle.symbolic)
.into(),
}
}
}
impl<Message: 'static> From<Icon> for Element<'static, Message> {
fn from(icon: Icon) -> Self {
icon.into_element::<Message>()
}
}

View file

@ -41,9 +41,9 @@ impl<'a, Message: 'static> ListColumn<'a, Message> {
#[must_use]
pub fn into_element(self) -> Element<'a, Message> {
iced::widget::column(self.children)
crate::widget::column::with_children(self.children)
.spacing(12)
.apply(iced::widget::container)
.apply(crate::widget::container)
.padding([16, 6])
.style(theme::Container::custom(style))
.into()
@ -58,9 +58,10 @@ impl<'a, Message: 'static> From<ListColumn<'a, Message>> for Element<'a, Message
#[must_use]
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn style(theme: &crate::Theme) -> iced::widget::container::Appearance {
pub fn style(theme: &crate::Theme) -> crate::widget::container::Appearance {
let container = &theme.current_container().component;
iced::widget::container::Appearance {
crate::widget::container::Appearance {
icon_color: Some(container.on.into()),
text_color: Some(container.on.into()),
background: Some(Background::Color(container.base.into())),
border_radius: 8.0.into(),

View file

@ -16,8 +16,8 @@ pub use iced::widget::{svg, Svg};
pub mod aspect_ratio;
mod button;
pub use button::*;
pub mod button;
pub use button::{button, Button, IconButton, LinkButton, TextButton};
pub mod card;
pub use card::*;

View file

@ -31,7 +31,6 @@ where
.button_height(32)
.button_padding([16, 10, 16, 10])
.button_spacing(8)
.icon_size(16)
.on_activate(on_activate)
.spacing(8)
.style(crate::theme::SegmentedButton::ViewSwitcher)
@ -46,6 +45,7 @@ where
pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance {
let cosmic = &theme.cosmic();
iced_style::container::Appearance {
icon_color: Some(cosmic.on_bg_color().into()),
text_color: Some(cosmic.on_bg_color().into()),
background: Some(Background::Color(cosmic.primary.base.into())),
border_radius: 8.0.into(),

View file

@ -3,13 +3,11 @@
//! A button for toggling the navigation side panel.
use crate::{theme, Element};
use crate::{widget, Element};
use apply::Apply;
use derive_setters::Setters;
use iced::Length;
use super::IconSource;
#[derive(Setters)]
pub struct NavBarToggle<Message> {
active: bool,
@ -27,26 +25,22 @@ pub fn nav_bar_toggle<Message>() -> NavBarToggle<Message> {
impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, Message> {
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
let mut widget = super::icon(
if nav_bar_toggle.active {
IconSource::svg_from_memory(&include_bytes!("../../res/sidebar-active.svg")[..])
} else {
IconSource::from("open-menu-symbolic")
},
16,
)
.style(theme::Svg::SymbolicActive)
.apply(iced::widget::container)
.apply(iced::widget::button)
.padding([8, 16, 8, 16])
.style(theme::Button::Text);
let icon = if nav_bar_toggle.active {
widget::icon::handle::from_svg_bytes(
&include_bytes!("../../res/sidebar-active.svg")[..],
)
.symbolic(true)
} else {
widget::icon::handle::from_name("open-menu-symbolic")
.size(16)
.handle()
};
if let Some(message) = nav_bar_toggle.on_toggle {
widget = widget.on_press(message);
}
widget
.apply(iced::widget::container)
widget::button::text("")
.leading_icon(icon)
.padding([8, 16, 8, 16])
.on_press_maybe(nav_bar_toggle.on_toggle)
.apply(widget::container)
.center_y()
.height(Length::Fill)
.into()

View file

@ -1,12 +1,8 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::iced::{
self,
widget::{container, Button},
Background, Length,
};
use crate::Renderer;
use crate::iced::{Background, Length};
use crate::widget::{container, icon, row, text_input};
use apply::Apply;
/// A search field for COSMIC applications.
@ -38,29 +34,29 @@ pub struct Field<'a, Message: 'static + Clone> {
impl<'a, Message: 'static + Clone> Field<'a, Message> {
pub fn into_element(mut self) -> crate::Element<'a, Message> {
let mut input = iced::widget::text_input("", self.phrase)
let input = text_input("", self.phrase)
.on_input(self.on_change)
.style(crate::theme::TextInput::Search)
.width(Length::Fill)
.id(self.id);
.id(self.id)
.on_submit_maybe(self.on_submit.take());
if let Some(message) = self.on_submit.take() {
input = input.on_submit(message);
}
iced::widget::row!(
super::icon::search(16),
input,
clear_button().on_press(self.on_clear)
)
.width(Length::Fixed(300.0))
.height(Length::Fixed(38.0))
.padding([0, 16])
.spacing(8)
.align_items(iced::Alignment::Center)
.apply(container)
.style(crate::theme::Container::custom(active_style))
.into()
row::with_capacity(3)
.push(
icon::handle::from_svg_bytes(&include_bytes!("search.svg")[..])
.symbolic(true)
.icon()
.size(16),
)
.push(input)
.push(clear_button().on_press(self.on_clear))
.width(Length::Fixed(300.0))
.height(Length::Fixed(38.0))
.padding([0, 16])
.spacing(8)
.align_items(iced::Alignment::Center)
.apply(container)
.style(crate::theme::Container::custom(active_style))
.into()
}
}
@ -70,11 +66,10 @@ impl<'a, Message: 'static + Clone> From<Field<'a, Message>> for crate::Element<'
}
}
fn clear_button<Message: 'static>() -> Button<'static, Message, Renderer> {
super::icon::edit_clear(16)
.style(crate::theme::Svg::Symbolic)
.apply(iced::widget::button)
.style(crate::theme::Button::Text)
fn clear_button<Message: 'static>() -> crate::widget::IconButton<'static, Message> {
icon::handle::from_name("edit-clear-symbolic")
.size(16)
.apply(crate::widget::button::icon)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
@ -83,6 +78,7 @@ fn active_style(theme: &crate::Theme) -> container::Appearance {
let mut neutral_7 = cosmic.palette.neutral_7;
neutral_7.alpha = 0.25;
iced::widget::container::Appearance {
icon_color: Some(cosmic.palette.neutral_9.into()),
text_color: Some(cosmic.palette.neutral_9.into()),
background: Some(Background::Color(neutral_7.into())),
border_radius: 24.0.into(),

View file

@ -43,16 +43,15 @@ mod field;
mod model;
mod button {
use crate::iced::{self, widget::container};
use crate::widget::{container, icon};
use apply::Apply;
/// A search button which converts to a search [`field`] on click.
#[must_use]
pub fn button<Message: 'static + Clone>(on_press: Message) -> crate::Element<'static, Message> {
super::icon::search(16)
.style(crate::theme::Svg::SymbolicActive)
.apply(iced::widget::button)
.style(crate::theme::Button::Text)
icon::handle::from_svg_bytes(&include_bytes!("search.svg")[..])
.symbolic(true)
.apply(crate::widget::button::icon)
.on_press(on_press)
.apply(container)
.padding([0, 0, 0, 11])
@ -60,23 +59,6 @@ mod button {
}
}
pub mod icon {
use crate::widget::IconSource;
#[must_use]
pub fn search(size: u16) -> crate::widget::Icon<'static> {
crate::widget::icon(
IconSource::svg_from_memory(&include_bytes!("search.svg")[..]),
size,
)
}
#[must_use]
pub fn edit_clear(size: u16) -> crate::widget::Icon<'static> {
crate::widget::icon(IconSource::from("edit-clear-symbolic"), size)
}
}
pub use button::button;
pub use field::{field, Field};
pub use model::Model;

View file

@ -11,8 +11,8 @@ use iced::{Length, Rectangle, Size};
use iced_core::layout;
/// Horizontal [`SegmentedButton`].
pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> =
SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>;
pub type HorizontalSegmentedButton<'a, SelectionMode, Message> =
SegmentedButton<'a, Horizontal, SelectionMode, Message>;
/// A type marker defining the horizontal variant of a [`SegmentedButton`].
pub struct Horizontal;
@ -21,36 +21,24 @@ pub struct Horizontal;
///
/// For details on the model, see the [`segmented_button`](super) module for more details.
#[must_use]
pub fn horizontal<SelectionMode: Default, Message, Renderer>(
pub fn horizontal<SelectionMode: Default, Message>(
model: &Model<SelectionMode>,
) -> SegmentedButton<Horizontal, SelectionMode, Message, Renderer>
) -> SegmentedButton<Horizontal, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Model<SelectionMode>: Selectable,
{
SegmentedButton::new(model)
}
impl<'a, SelectionMode, Message, Renderer> SegmentedVariant
for SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>
impl<'a, SelectionMode, Message> SegmentedVariant
for SegmentedButton<'a, Horizontal, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
type Renderer = Renderer;
fn variant_appearance(
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
theme: &crate::Theme,
style: &crate::theme::SegmentedButton,
) -> super::Appearance {
theme.horizontal(style)
}
@ -73,7 +61,7 @@ where
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn variant_layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node {
let limits = limits.width(self.width);
let (mut width, height) = self.max_button_dimensions(renderer, limits.max());

View file

@ -97,11 +97,3 @@ pub type SecondaryMap<T> = slotmap::SecondaryMap<Entity, T>;
///
/// Sparse maps internally use a `HashMap`, for data that is sparsely associated.
pub type SparseSecondaryMap<T> = slotmap::SparseSecondaryMap<Entity, T>;
/// Defines the color of the icon for a segmented item.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum IconColor {
#[default]
None,
Color(crate::iced::Color),
}

View file

@ -1,11 +1,10 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced::Color;
use slotmap::{SecondaryMap, SparseSecondaryMap};
use super::{Entity, Model, Selectable};
use crate::widget::IconSource;
use crate::widget::icon::Icon;
use std::borrow::Cow;
/// A builder for a [`Model`].
@ -104,18 +103,11 @@ where
/// .build()
/// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn icon(mut self, icon: impl Into<IconSource<'static>>) -> Self {
pub fn icon(mut self, icon: Icon) -> Self {
self.model.0.icon_set(self.id, icon);
self
}
/// Defines the color of an icon.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn icon_color(mut self, icon: Option<Color>) -> Self {
self.model.0.icon_color_set(self.id, icon);
self
}
/// Define the position of the newly-inserted item.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn position(mut self, position: u16) -> Self {

View file

@ -3,10 +3,9 @@
use std::borrow::Cow;
use iced::Color;
use slotmap::{SecondaryMap, SparseSecondaryMap};
use crate::widget::IconSource;
use crate::widget::Icon;
use super::{Entity, Model, Selectable};
@ -90,18 +89,11 @@ where
/// model.insert().text("Item A").icon(IconSource::from("icon-a"));
/// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn icon(self, icon: impl Into<IconSource<'static>>) -> Self {
pub fn icon(self, icon: Icon) -> Self {
self.model.icon_set(self.id, icon);
self
}
/// Define the color for the icon.
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn icon_color(self, icon: Option<Color>) -> Self {
self.model.icon_color_set(self.id, icon);
self
}
/// Returns the ID of the item that was inserted.
///
/// ```ignore

View file

@ -10,15 +10,12 @@ pub use self::entity::EntityMut;
mod selection;
pub use self::selection::{MultiSelect, Selectable, SingleSelect};
use crate::widget::IconSource;
use iced::Color;
use crate::widget::Icon;
use slotmap::{SecondaryMap, SlotMap};
use std::any::{Any, TypeId};
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use super::IconColor;
slotmap::new_key_type! {
/// A unique ID for an item in the [`Model`].
pub struct Entity;
@ -62,7 +59,7 @@ pub struct Model<SelectionMode: Default> {
pub(super) items: SlotMap<Entity, Settings>,
/// Icons optionally-defined for each item.
pub(super) icons: SecondaryMap<Entity, IconSource<'static>>,
pub(super) icons: SecondaryMap<Entity, Icon>,
/// Text optionally-defined for each item.
pub(super) text: SecondaryMap<Entity, Cow<'static, str>>,
@ -224,7 +221,7 @@ where
/// println!("has icon: {:?}", icon);
/// }
/// ```
pub fn icon(&self, id: Entity) -> Option<&IconSource<'static>> {
pub fn icon(&self, id: Entity) -> Option<&Icon> {
self.icons.get(id)
}
@ -235,34 +232,12 @@ where
/// println!("previously had icon: {:?}", old_icon);
/// }
/// ```
pub fn icon_set(
&mut self,
id: Entity,
icon: impl Into<IconSource<'static>>,
) -> Option<IconSource<'static>> {
pub fn icon_set(&mut self, id: Entity, icon: Icon) -> Option<Icon> {
if !self.contains_item(id) {
return None;
}
self.icons.insert(id, icon.into())
}
/// Sets the color of the icon. By default, the color matches the text.
pub fn icon_color_set(&mut self, id: Entity, color: Option<Color>) {
if self.contains_item(id) {
self.data_set(
id,
match color {
Some(color) => IconColor::Color(color),
None => IconColor::None,
},
);
}
}
/// Unsets the defined color of an icon.
pub fn icon_color_remove(&mut self, id: Entity) {
self.data_remove::<IconColor>(id);
self.icons.insert(id, icon)
}
/// Removes the icon from an item.
@ -271,7 +246,7 @@ where
/// if let Some(old_icon) = model.icon_remove(id) {
/// println!("previously had icon: {:?}", old_icon);
/// }
pub fn icon_remove(&mut self, id: Entity) -> Option<IconSource<'static>> {
pub fn icon_remove(&mut self, id: Entity) -> Option<Icon> {
self.icons.remove(id)
}

View file

@ -14,44 +14,32 @@ use iced_core::layout;
pub struct Vertical;
/// Vertical [`SegmentedButton`].
pub type VerticalSegmentedButton<'a, SelectionMode, Message, Renderer> =
SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>;
pub type VerticalSegmentedButton<'a, SelectionMode, Message> =
SegmentedButton<'a, Vertical, SelectionMode, Message>;
/// Vertical implementation of the [`SegmentedButton`].
///
/// For details on the model, see the [`segmented_button`](super) module for more details.
#[must_use]
pub fn vertical<SelectionMode, Message, Renderer>(
pub fn vertical<SelectionMode, Message>(
model: &Model<SelectionMode>,
) -> SegmentedButton<Vertical, SelectionMode, Message, Renderer>
) -> SegmentedButton<Vertical, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
SegmentedButton::new(model)
}
impl<'a, SelectionMode, Message, Renderer> SegmentedVariant
for SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>
impl<'a, SelectionMode, Message> SegmentedVariant
for SegmentedButton<'a, Vertical, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
type Renderer = Renderer;
fn variant_appearance(
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
theme: &crate::Theme,
style: &crate::theme::SegmentedButton,
) -> super::Appearance {
theme.vertical(style)
}
@ -74,7 +62,7 @@ where
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn variant_layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node {
let limits = limits.width(self.width);
let (width, mut height) = self.max_button_dimensions(renderer, limits.max());

View file

@ -2,18 +2,18 @@
// SPDX-License-Identifier: MPL-2.0
use super::model::{Entity, Model, Selectable};
use super::style::StyleSheet;
use super::IconColor;
use crate::widget::{icon, IconSource};
use crate::theme::SegmentedButton as Style;
use crate::widget::{icon, Icon};
use crate::{Element, Renderer};
use derive_setters::Setters;
use iced::{
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
Rectangle, Size,
alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle,
Size,
};
use iced_core::text::{LineHeight, Shaping};
use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping};
use iced_core::widget::{self, operation, tree};
use iced_core::BorderRadius;
use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
use iced_core::{BorderRadius, Point, Renderer as IcedRenderer};
use std::marker::PhantomData;
/// State that is maintained by each individual widget.
@ -47,32 +47,23 @@ impl operation::Focusable for LocalState {
/// Isolates variant-specific behaviors from [`SegmentedButton`].
pub trait SegmentedVariant {
type Renderer: iced_core::Renderer;
/// Get the appearance for this variant of the widget.
fn variant_appearance(
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
) -> super::Appearance
where
<Self::Renderer as iced_core::Renderer>::Theme: StyleSheet;
theme: &crate::Theme,
style: &crate::theme::SegmentedButton,
) -> super::Appearance;
/// 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;
fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node;
}
/// A conjoined group of items that function together as a button.
#[derive(Setters)]
pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
pub struct SegmentedButton<'a, Variant, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
@ -82,7 +73,7 @@ where
/// iced widget ID
pub(super) id: Option<Id>,
/// The icon used for the close button.
pub(super) close_icon: IconSource<'a>,
pub(super) close_icon: Icon,
/// Show the close icon only when item is hovered.
pub(super) show_close_icon_on_hover: bool,
/// Padding around a button.
@ -92,15 +83,13 @@ where
/// Spacing between icon and text in button.
pub(super) button_spacing: u16,
/// Desired font for active tabs.
pub(super) font_active: Option<Renderer::Font>,
pub(super) font_active: Option<crate::font::Font>,
/// Desired font for hovered tabs.
pub(super) font_hovered: Option<Renderer::Font>,
pub(super) font_hovered: Option<crate::font::Font>,
/// Desired font for inactive tabs.
pub(super) font_inactive: Option<Renderer::Font>,
pub(super) font_inactive: Option<crate::font::Font>,
/// Size of the font.
pub(super) font_size: f32,
/// Size of icon
pub(super) icon_size: u16,
/// Desired width of the widget.
pub(super) width: Length,
/// Desired height of the widget.
@ -111,7 +100,7 @@ where
pub(super) line_height: LineHeight,
/// Style to draw the widget in.
#[setters(into)]
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
pub(super) style: Style,
/// Emits the ID of the item that was activated.
#[setters(strip_option)]
pub(super) on_activate: Option<fn(Entity) -> Message>,
@ -122,15 +111,9 @@ where
variant: PhantomData<Variant>,
}
impl<'a, Variant, SelectionMode, Message, Renderer>
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Self: SegmentedVariant<Renderer = Renderer>,
Self: SegmentedVariant,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
@ -139,7 +122,9 @@ where
Self {
model,
id: None,
close_icon: IconSource::from("window-close-symbolic"),
close_icon: icon::handle::from_name("window-close-symbolic")
.size(16)
.icon(),
show_close_icon_on_hover: false,
button_padding: [4, 4, 4, 4],
button_height: 32,
@ -148,12 +133,11 @@ where
font_hovered: None,
font_inactive: None,
font_size: 14.0,
icon_size: 16,
height: Length::Shrink,
width: Length::Fill,
spacing: 0,
line_height: LineHeight::default(),
style: <Renderer::Theme as StyleSheet>::Style::default(),
style: Style::default(),
on_activate: None,
on_close: None,
variant: PhantomData,
@ -238,15 +222,16 @@ where
}
// Add icon to measurement if icon was given.
if self.model.icon(key).is_some() {
button_height = button_height.max(f32::from(self.icon_size));
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
if let Some(icon) = self.model.icon(key) {
button_height = button_height.max(f32::from(icon.size));
button_width += f32::from(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));
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing) + 8.0;
button_height = button_height.max(f32::from(self.close_icon.size));
button_width +=
f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0;
}
height = height.max(button_height);
@ -262,15 +247,10 @@ where
}
}
impl<'a, Variant, SelectionMode, Message, Renderer> Widget<Message, Renderer>
for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
impl<'a, Variant, SelectionMode, Message> Widget<Message, Renderer>
for SegmentedButton<'a, Variant, SelectionMode, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer,
Renderer::Theme: StyleSheet,
Self: SegmentedVariant<Renderer = Renderer>,
Self: SegmentedVariant,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
Message: 'static + Clone,
@ -325,7 +305,7 @@ where
if let Some(on_close) = self.on_close.as_ref() {
if cursor_position.is_over(close_bounds(
bounds,
f32::from(self.icon_size),
f32::from(self.close_icon.size),
self.button_padding,
)) {
if let Event::Mouse(mouse::Event::ButtonReleased(
@ -429,11 +409,11 @@ where
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &<Renderer as iced_core::Renderer>::Theme,
_style: &renderer::Style,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: mouse::Cursor,
_viewport: &iced::Rectangle,
cursor: mouse::Cursor,
viewport: &iced::Rectangle,
) {
let state = tree.state.downcast_ref::<LocalState>();
let appearance = Self::variant_appearance(theme, &self.style);
@ -479,12 +459,6 @@ where
status_appearance.middle
};
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),
};
// Render the background of the button.
if status_appearance.background.is_some() {
renderer.fill_quad(
@ -530,27 +504,29 @@ where
bounds.height -=
f32::from(self.button_padding[1]) - f32::from(self.button_padding[3]);
let width = f32::from(self.icon_size);
let width = f32::from(icon.size);
let offset = width + f32::from(self.button_spacing);
bounds.y = y - width / 2.0;
let icon_bounds = Rectangle {
let mut layout_node = layout::Node::new(Size {
width,
height: width,
..bounds
};
height: width - offset,
});
layout_node.move_to(Point {
x: bounds.x + offset,
y: bounds.y,
});
bounds.x += offset;
bounds.width -= offset;
match icon.load(self.icon_size, None, false, true) {
icon::Handle::Image(_handle) => {
unimplemented!()
}
icon::Handle::Svg(handle) => {
iced_core::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds);
}
}
Widget::<Message, Renderer>::draw(
&Element::<Message>::from(icon.clone()),
&Tree::empty(),
renderer,
theme,
style,
Layout::new(&layout_node),
cursor,
viewport,
);
alignment::Horizontal::Left
} else {
@ -581,22 +557,27 @@ where
// Draw a close button if this is set.
if show_close_button {
let width = f32::from(self.icon_size);
let width = f32::from(self.close_icon.size);
let icon_bounds = close_bounds(original_bounds, width, self.button_padding);
let mut layout_node = layout::Node::new(Size {
width: icon_bounds.width,
height: icon_bounds.height,
});
layout_node.move_to(Point {
x: icon_bounds.x,
y: icon_bounds.y,
});
match self.close_icon.load(self.icon_size, None, false, true) {
icon::Handle::Image(_handle) => {
unimplemented!()
}
icon::Handle::Svg(handle) => {
iced_core::svg::Renderer::draw(
renderer,
handle,
Some(status_appearance.text_color),
icon_bounds,
);
}
}
Widget::<Message, Renderer>::draw(
&Element::<Message>::from(self.close_icon.clone()),
&Tree::empty(),
renderer,
theme,
style,
Layout::new(&layout_node),
cursor,
viewport,
);
}
}
}
@ -611,24 +592,16 @@ where
}
}
impl<'a, Variant, SelectionMode, Message, Renderer>
From<SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>>
for Element<'a, Message, Renderer>
impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
for Element<'a, Message>
where
Renderer: iced_core::Renderer
+ iced_core::text::Renderer
+ iced_core::image::Renderer
+ iced_core::svg::Renderer
+ 'a,
Renderer::Theme: StyleSheet,
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>:
SegmentedVariant<Renderer = Renderer>,
SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
Variant: 'static,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
Message: 'static + Clone,
{
fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>) -> Self {
fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message>) -> Self {
if widget.model.items.is_empty() {
widget.spacing = 0;
}

View file

@ -17,7 +17,7 @@ use super::segmented_button::{
#[must_use]
pub fn horizontal<SelectionMode: Default, Message>(
model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
) -> HorizontalSegmentedButton<SelectionMode, Message>
where
Model<SelectionMode>: Selectable,
{
@ -36,7 +36,7 @@ where
#[must_use]
pub fn vertical<SelectionMode, Message>(
model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
) -> VerticalSegmentedButton<SelectionMode, Message>
where
Model<SelectionMode>: Selectable,
SelectionMode: Default,

View file

@ -3,16 +3,18 @@
use std::borrow::Cow;
use crate::{widget::text, Element, Renderer};
use crate::{
widget::{column, horizontal_space, row, text, Row},
Element, Renderer,
};
use derive_setters::Setters;
use iced::widget::{column, horizontal_space, row, Row};
/// A settings item aligned in a row
#[must_use]
#[allow(clippy::module_name_repetitions)]
pub fn item<'a, Message: 'static>(
title: impl Into<Cow<'a, str>>,
widget: impl Into<Element<'a, Message>>,
title: impl Into<Cow<'a, str>> + 'a,
widget: impl Into<Element<'a, Message>> + 'a,
) -> Row<'a, Message, Renderer> {
item_row(vec![
text(title).into(),
@ -25,7 +27,7 @@ pub fn item<'a, Message: 'static>(
#[must_use]
#[allow(clippy::module_name_repetitions)]
pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Renderer> {
row(children)
row::with_children(children)
.align_items(iced::Alignment::Center)
.padding([0, 18])
.spacing(12)
@ -65,10 +67,12 @@ impl<'a, Message: 'static> Item<'a, Message> {
}
if let Some(description) = self.description {
let title = text(self.title);
let desc = text(description).size(10);
let column = column::with_capacity(2)
.spacing(2)
.push(text(self.title))
.push(text(description).size(10));
contents.push(column!(title, desc).spacing(2).into());
contents.push(column.into());
} else {
contents.push(text(self.title).into());
}

View file

@ -7,11 +7,14 @@ mod section;
pub use self::item::{item, item_row};
pub use self::section::{view_section, Section};
use crate::widget::{column, Column};
use crate::{Element, Renderer};
use iced::widget::{column, Column};
/// A column with a predefined style for creating a settings panel
#[must_use]
pub fn view_column<Message: 'static>(children: Vec<Element<Message>>) -> Column<Message, Renderer> {
column(children).spacing(24).padding([0, 24]).max_width(678)
column::with_children(children)
.spacing(24)
.padding([0, 24])
.max_width(678)
}

View file

@ -1,9 +1,8 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::widget::ListColumn;
use crate::widget::{column, text, ListColumn};
use crate::Element;
use iced::widget::{column, text};
use std::borrow::Cow;
/// A section within a settings view column.
@ -31,10 +30,10 @@ impl<'a, Message: 'static> Section<'a, Message> {
impl<'a, Message: 'static> From<Section<'a, Message>> for Element<'a, Message> {
fn from(data: Section<'a, Message>) -> Self {
let title = text(data.title).font(crate::font::FONT_SEMIBOLD).into();
column(vec![title, data.children.into_element()])
column::with_capacity(2)
.spacing(8)
.push(text(data.title).font(crate::font::FONT_SEMIBOLD))
.push(data.children)
.into()
}
}

View file

@ -6,12 +6,11 @@ use std::borrow::Cow;
pub use self::model::{Message, Model};
use crate::widget::{icon, text};
use crate::widget::{button, container, icon, row, text};
use crate::{theme, Element};
use apply::Apply;
use iced::{
alignment::{Horizontal, Vertical},
widget::{button, container, row},
Alignment, Length,
};
@ -42,9 +41,10 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
pub fn into_element(self) -> Element<'a, Message> {
let Self { on_change, label } = self;
container(
row![
icon("list-remove-symbolic", 24)
.style(theme::Svg::Symbolic)
row::with_children(vec![
icon::handle::from_name("list-remove-symbolic")
.size(24)
.icon()
.apply(container)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0))
@ -54,14 +54,17 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0))
.style(theme::Button::Text)
.on_press(model::Message::Decrement),
.on_press(model::Message::Decrement)
.into(),
text(label)
.vertical_alignment(Vertical::Center)
.apply(container)
.align_x(Horizontal::Center)
.align_y(Vertical::Center),
icon("list-add-symbolic", 24)
.style(theme::Svg::Symbolic)
.align_y(Vertical::Center)
.into(),
icon::handle::from_name("list-add-symbolic")
.size(24)
.icon()
.apply(container)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0))
@ -71,8 +74,9 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0))
.style(theme::Button::Text)
.on_press(model::Message::Increment),
]
.on_press(model::Message::Increment)
.into(),
])
.width(Length::Shrink)
.height(Length::Fixed(32.0))
.spacing(4.0)
@ -101,6 +105,7 @@ fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance {
let accent = &basic.accent;
let corners = &basic.corner_radii;
iced_style::container::Appearance {
icon_color: Some(basic.palette.neutral_10.into()),
text_color: Some(basic.palette.neutral_10.into()),
background: None,
border_radius: corners.radius_s.into(),

View file

@ -1,3 +1,7 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
//! Track the cursor of a text input.
use super::value::Value;

View file

@ -1,3 +1,7 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
use super::{cursor::Cursor, value::Value};
pub struct Editor<'a> {

View file

@ -1,3 +1,7 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
//! Display fields that can be filled with text.
//!
//! A [`TextInput`] has some local [`State`].
@ -9,6 +13,7 @@ use super::editor::Editor;
use super::style::StyleSheet;
pub use super::value::Value;
use apply::Apply;
use iced::Limits;
use iced_core::event::{self, Event};
use iced_core::keyboard;
@ -66,8 +71,9 @@ where
.style(super::style::TextInput::Search)
.start_icon(
iced_widget::container(
crate::widget::icon("system-search-symbolic", 16)
.style(crate::theme::Svg::Symbolic),
crate::widget::icon::handle::from_name("system-search-symbolic")
.size(16)
.icon(),
)
.padding([spacing, spacing, spacing, spacing])
.into(),
@ -75,8 +81,10 @@ where
if let Some(msg) = on_clear {
input.end_icon(
crate::widget::button::button(crate::theme::Button::Text)
.icon(crate::theme::Svg::Symbolic, "edit-clear-symbolic", 16)
crate::widget::icon::handle::from_name("edit-clear-symbolic")
.size(16)
.handle()
.apply(crate::widget::button::icon)
.on_press(msg)
.padding([spacing, spacing, spacing, spacing])
.into(),
@ -102,24 +110,22 @@ where
.padding([0, spacing, 0, spacing])
.style(super::style::TextInput::Default)
.start_icon(
iced_widget::container(
crate::widget::icon("system-lock-screen-symbolic", 16)
.style(crate::theme::Svg::Symbolic),
)
.padding([spacing, spacing, spacing, spacing])
.into(),
crate::widget::icon::handle::from_name("system-lock-screen-symbolic")
.size(16)
.icon()
.apply(iced_widget::container)
.padding([spacing, spacing, spacing, spacing])
.into(),
);
if hidden {
input = input.password();
}
if let Some(msg) = on_visible_toggle {
input.end_icon(
crate::widget::button::button(crate::theme::Button::Text)
.icon(
crate::theme::Svg::Symbolic,
"document-properties-symbolic",
16,
)
crate::widget::icon::handle::from_name("document-properties-symbolic")
.size(16)
.handle()
.apply(crate::widget::button::icon)
.on_press(msg)
.padding([spacing, spacing, spacing, spacing])
.into(),
@ -300,8 +306,14 @@ where
/// Sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
pub fn on_submit(mut self, message: Message) -> Self {
self.on_submit = Some(message);
pub fn on_submit(self, message: Message) -> Self {
self.on_submit_maybe(Some(message))
}
/// Maybe sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self {
self.on_submit = message;
self
}
@ -1846,6 +1858,7 @@ pub fn draw<'a, Message>(
renderer,
theme,
&renderer::Style {
icon_color: renderer_style.icon_color,
text_color: appearance.text_color,
scale_factor: renderer_style.scale_factor,
},
@ -2013,6 +2026,7 @@ pub fn draw<'a, Message>(
renderer,
theme,
&renderer::Style {
icon_color: renderer_style.icon_color,
text_color: appearance.text_color,
scale_factor: renderer_style.scale_factor,
},
@ -2063,11 +2077,9 @@ pub struct TextInputString(String);
#[cfg(feature = "wayland")]
impl DataFromMimeType for TextInputString {
fn from_mime_type(&self, mime_type: &str) -> Option<Vec<u8>> {
if SUPPORTED_MIME_TYPES.contains(&mime_type) {
Some(self.0.as_bytes().to_vec())
} else {
None
}
SUPPORTED_MIME_TYPES
.contains(&mime_type)
.then(|| self.0.as_bytes().to_vec())
}
}

View file

@ -1,3 +1,7 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
//! A text input widget from iced widgets plus some added details.
pub mod cursor;

View file

@ -1,3 +1,7 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
//! Change the appearance of a text input.
use iced_core::{Background, BorderRadius, Color};

View file

@ -1,3 +1,7 @@
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MIT
use unicode_segmentation::UnicodeSegmentation;
/// The value of a [`TextInput`].

View file

@ -17,7 +17,7 @@ use super::segmented_button::{
#[must_use]
pub fn horizontal<SelectionMode: Default, Message>(
model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
) -> HorizontalSegmentedButton<SelectionMode, Message>
where
Model<SelectionMode>: Selectable,
{
@ -36,7 +36,7 @@ where
#[must_use]
pub fn vertical<SelectionMode, Message>(
model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
) -> VerticalSegmentedButton<SelectionMode, Message>
where
Model<SelectionMode>: Selectable,
SelectionMode: Default,

View file

@ -1,13 +1,11 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::borrow::Cow;
use iced::{alignment, widget, Alignment, Background, Color, Length};
use crate::{theme, Element, Renderer, Theme};
use super::icon;
use crate::{theme, widget, Element, Renderer, Theme};
use apply::Apply;
use iced::{alignment, Alignment, Background, Color, Length};
use std::borrow::Cow;
#[must_use]
pub fn warning<'a, Message>(message: impl Into<Cow<'a, str>>) -> Warning<'a, Message> {
@ -32,29 +30,22 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> {
/// A custom button that has the desired default spacing and padding.
pub fn into_widget(self) -> widget::Container<'a, Message, Renderer> {
let close_button =
widget::button(icon("window-close-symbolic", 16).style(theme::Svg::Default))
.style(theme::Button::Transparent);
let label = widget::container(crate::widget::text(self.message)).width(Length::Fill);
let close_button = if let Some(message) = self.on_close {
close_button.on_press(message)
} else {
close_button
};
let close_button = icon::handle::from_name("window-close-symbolic")
.size(16)
.apply(widget::button::icon)
.on_press_maybe(self.on_close);
widget::container(
widget::row(vec![
widget::container(crate::widget::text(self.message))
.width(Length::Fill)
.into(),
close_button.into(),
])
.align_items(Alignment::Center),
)
.style(theme::Container::custom(warning_container))
.padding(10)
.align_y(alignment::Vertical::Center)
.width(Length::Fill)
widget::row::with_capacity(2)
.push(label)
.push(close_button)
.align_items(Alignment::Center)
.apply(widget::container)
.style(theme::Container::custom(warning_container))
.padding(10)
.align_y(alignment::Vertical::Center)
.width(Length::Fill)
}
}
@ -67,6 +58,7 @@ impl<'a, Message: 'static + Clone> From<Warning<'a, Message>> for Element<'a, Me
#[must_use]
pub fn warning_container(theme: &Theme) -> widget::container::Appearance {
widget::container::Appearance {
icon_color: Some(theme.cosmic().warning.on.into()),
text_color: Some(theme.cosmic().warning.on.into()),
background: Some(Background::Color(theme.cosmic().warning_color().into())),
border_radius: 0.0.into(),