feat!(widget): rewrite button & icon widget APIs
This commit is contained in:
parent
18debe546d
commit
4e4eeaac12
60 changed files with 2191 additions and 1113 deletions
|
|
@ -125,9 +125,9 @@ impl cosmic::Application for App {
|
||||||
|
|
||||||
let text = cosmic::widget::text(page_content);
|
let text = cosmic::widget::text(page_content);
|
||||||
|
|
||||||
let centered = iced::widget::container(text)
|
let centered = cosmic::widget::container(text)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.height(iced::Length::Fill)
|
.height(iced::Length::Shrink)
|
||||||
.align_x(iced::alignment::Horizontal::Center)
|
.align_x(iced::alignment::Horizontal::Center)
|
||||||
.align_y(iced::alignment::Vertical::Center);
|
.align_y(iced::alignment::Vertical::Center);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@ edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcosmic = { path = "../..", default-features = false, features = ["wayland", "tokio"] }
|
libcosmic = { path = "../..", features = ["wayland", "tokio"] }
|
||||||
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"] }
|
||||||
|
# cosmic-time = { path = "../../../cosmic-time", default-features = false, features = ["libcosmic", "once_cell"]}
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
# COSMIC
|
# Deprecated
|
||||||
An example of the COSMIC design system.
|
|
||||||
|
|
||||||
All the example code is located in the __[`main`](src/main.rs)__ file.
|
This example will be removed once its contents are migrated to the design demo.
|
||||||
|
|
||||||
You can run it with `cargo run`:
|
|
||||||
```
|
|
||||||
cargo run --package cosmic --release
|
|
||||||
```
|
|
||||||
|
|
@ -10,22 +10,21 @@ use cosmic::{
|
||||||
},
|
},
|
||||||
iced_futures::Subscription,
|
iced_futures::Subscription,
|
||||||
iced_style::application,
|
iced_style::application,
|
||||||
iced_widget::text,
|
prelude::*,
|
||||||
theme::{self, Theme},
|
theme::{self, Theme},
|
||||||
widget::{
|
widget::{
|
||||||
button, cosmic_container, header_bar, icon, inline_input, nav_bar, nav_bar_toggle,
|
button, cosmic_container, header_bar, icon, inline_input, nav_bar, nav_bar_toggle,
|
||||||
rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate},
|
rectangle_tracker::{rectangle_tracker_subscription, RectangleTracker, RectangleUpdate},
|
||||||
scrollable, search_input, secure_input, segmented_button, segmented_selection, settings,
|
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 cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
||||||
use std::{
|
use std::{
|
||||||
sync::atomic::{AtomicU32, Ordering},
|
sync::atomic::{AtomicU32, Ordering},
|
||||||
vec,
|
vec,
|
||||||
};
|
};
|
||||||
use theme::Button as ButtonTheme;
|
|
||||||
|
|
||||||
static DEBUG_TOGGLER: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
static DEBUG_TOGGLER: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||||
static 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
|
self.nav_bar_pages
|
||||||
.insert()
|
.insert()
|
||||||
.text(page.title())
|
.text(page.title())
|
||||||
.icon(IconSource::from(page.icon_name()))
|
.icon(icon::handle::from_name(page.icon_name()).icon())
|
||||||
.data(page)
|
.data(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,15 +372,6 @@ impl Application for Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !nav_bar_toggled {
|
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![
|
let content: Element<_> = settings::view_column(vec![
|
||||||
settings::view_section("Debug")
|
settings::view_section("Debug")
|
||||||
.add(settings::item(
|
.add(settings::item(
|
||||||
|
|
@ -396,34 +386,6 @@ impl Application for Window {
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.into(),
|
.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")
|
settings::view_section("Controls")
|
||||||
.add(settings::item(
|
.add(settings::item(
|
||||||
"Toggler",
|
"Toggler",
|
||||||
|
|
@ -567,6 +529,7 @@ impl Application for Window {
|
||||||
fn style(&self) -> <Self::Theme as cosmic::iced_style::application::StyleSheet>::Style {
|
fn style(&self) -> <Self::Theme as cosmic::iced_style::application::StyleSheet>::Style {
|
||||||
cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance {
|
cosmic::theme::Application::Custom(Box::new(|theme| application::Appearance {
|
||||||
background_color: Color::TRANSPARENT,
|
background_color: Color::TRANSPARENT,
|
||||||
|
icon_color: theme.cosmic().on_bg_color().into(),
|
||||||
text_color: theme.cosmic().on_bg_color().into(),
|
text_color: theme.cosmic().on_bg_color().into(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ publish = false
|
||||||
[dependencies]
|
[dependencies]
|
||||||
apply = "0.3.0"
|
apply = "0.3.0"
|
||||||
fraction = "0.13.0"
|
fraction = "0.13.0"
|
||||||
libcosmic = { path = "../..", default-features = false, features = ["debug", "winit"] }
|
libcosmic = { path = "../..", features = ["debug", "winit", "tokio"] }
|
||||||
once_cell = "1.18"
|
once_cell = "1.18"
|
||||||
slotmap = "1.0.6"
|
slotmap = "1.0.6"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
log = "0.4.17"
|
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"] }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
# COSMIC
|
# Deprecated
|
||||||
An example of the COSMIC design system.
|
|
||||||
|
|
||||||
All the example code is located in the __[`main`](src/main.rs)__ file.
|
This example will be removed once its contents are migrated to the design demo.
|
||||||
|
|
||||||
You can run it with `cargo run`:
|
|
||||||
```
|
|
||||||
cargo run --package cosmic --release
|
|
||||||
```
|
|
||||||
|
|
@ -13,12 +13,13 @@ use cosmic::{
|
||||||
window::{self, close, drag, minimize, toggle_maximize},
|
window::{self, close, drag, minimize, toggle_maximize},
|
||||||
},
|
},
|
||||||
keyboard_nav,
|
keyboard_nav,
|
||||||
|
prelude::*,
|
||||||
theme::{self, Theme},
|
theme::{self, Theme},
|
||||||
widget::{
|
widget::{
|
||||||
header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings,
|
button, header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button,
|
||||||
warning, IconSource,
|
settings, warning,
|
||||||
},
|
},
|
||||||
Element, ElementExt,
|
Element,
|
||||||
};
|
};
|
||||||
use cosmic_time::{Instant, Timeline};
|
use cosmic_time::{Instant, Timeline};
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -224,7 +225,7 @@ impl Window {
|
||||||
self.nav_bar
|
self.nav_bar
|
||||||
.insert()
|
.insert()
|
||||||
.text(page.title())
|
.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)
|
.secondary(&mut self.nav_id_to_page, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,14 +248,10 @@ impl Window {
|
||||||
) -> Element<Message> {
|
) -> Element<Message> {
|
||||||
let page = sub_page.parent_page();
|
let page = sub_page.parent_page();
|
||||||
column!(
|
column!(
|
||||||
iced::widget::Button::new(row!(
|
button::icon(icon::handle::from_name("go-previous-symbolic").size(16))
|
||||||
icon("go-previous-symbolic", 16).style(theme::Svg::SymbolicLink),
|
.label(page.title())
|
||||||
text(page.title()).size(14),
|
.padding(0)
|
||||||
))
|
.on_press(Message::from(page)),
|
||||||
.padding(0)
|
|
||||||
.style(theme::Button::Link)
|
|
||||||
// .id(BTN.clone())
|
|
||||||
.on_press(Message::from(page)),
|
|
||||||
row!(
|
row!(
|
||||||
text(sub_page.title()).size(28),
|
text(sub_page.title()).size(28),
|
||||||
horizontal_space(Length::Fill),
|
horizontal_space(Length::Fill),
|
||||||
|
|
@ -276,8 +273,9 @@ impl Window {
|
||||||
iced::widget::Button::new(
|
iced::widget::Button::new(
|
||||||
container(
|
container(
|
||||||
settings::item_row(vec![
|
settings::item_row(vec![
|
||||||
icon(sub_page.icon_name(), 20)
|
icon::handle::from_name(sub_page.icon_name())
|
||||||
.style(theme::Svg::Symbolic)
|
.size(20)
|
||||||
|
.icon()
|
||||||
.into(),
|
.into(),
|
||||||
column!(
|
column!(
|
||||||
text(sub_page.title()).size(14),
|
text(sub_page.title()).size(14),
|
||||||
|
|
@ -286,8 +284,9 @@ impl Window {
|
||||||
.spacing(2)
|
.spacing(2)
|
||||||
.into(),
|
.into(),
|
||||||
horizontal_space(iced::Length::Fill).into(),
|
horizontal_space(iced::Length::Fill).into(),
|
||||||
icon("go-next-symbolic", 20)
|
icon::handle::from_name("go-next-symbolic")
|
||||||
.style(theme::Svg::Symbolic)
|
.size(20)
|
||||||
|
.icon()
|
||||||
.into(),
|
.into(),
|
||||||
])
|
])
|
||||||
.spacing(16),
|
.spacing(16),
|
||||||
|
|
@ -296,7 +295,7 @@ impl Window {
|
||||||
.style(theme::Container::custom(list::column::style)),
|
.style(theme::Container::custom(list::column::style)),
|
||||||
)
|
)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.style(theme::Button::Transparent)
|
.style(theme::IcedButton::Transparent)
|
||||||
.on_press(Message::from(sub_page.into_page()))
|
.on_press(Message::from(sub_page.into_page()))
|
||||||
// .id(BTN.clone())
|
// .id(BTN.clone())
|
||||||
.into()
|
.into()
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ use cosmic::{
|
||||||
iced::{id, Alignment, Length},
|
iced::{id, Alignment, Length},
|
||||||
theme::{self, Button as ButtonTheme, ThemeType},
|
theme::{self, Button as ButtonTheme, ThemeType},
|
||||||
widget::{
|
widget::{
|
||||||
button, container, icon, segmented_button, segmented_selection, settings, spin_button,
|
button, cosmic_container::container, icon, segmented_button, segmented_selection, settings,
|
||||||
toggler, view_switcher,
|
spin_button, toggler, view_switcher,
|
||||||
},
|
},
|
||||||
Element,
|
Element,
|
||||||
};
|
};
|
||||||
|
|
@ -186,7 +186,7 @@ impl State {
|
||||||
Message::IconTheme(key) => {
|
Message::IconTheme(key) => {
|
||||||
self.icon_themes.activate(key);
|
self.icon_themes.activate(key);
|
||||||
if let Some(theme) = self.icon_themes.text(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) => {
|
Message::InputChanged(s) => {
|
||||||
|
|
@ -255,45 +255,13 @@ impl State {
|
||||||
"Scaling Factor",
|
"Scaling Factor",
|
||||||
spin_button(&window.scale_factor_string, Message::ScalingFactor),
|
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![
|
.add(settings::item_row(vec![
|
||||||
button(ButtonTheme::Primary)
|
cosmic::widget::button::destructive("Do not Touch")
|
||||||
.text("Primary")
|
.trailing_icon(
|
||||||
.on_press(Message::ButtonPressed)
|
icon::handle::from_name("dialog-warning-symbolic").size(16),
|
||||||
|
)
|
||||||
|
.on_press(Message::ToggleWarning)
|
||||||
.into(),
|
.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(),
|
.into(),
|
||||||
settings::view_section("Controls")
|
settings::view_section("Controls")
|
||||||
|
|
@ -454,9 +422,13 @@ impl State {
|
||||||
"Primary container with some text and a couple icons testing default fallbacks"
|
"Primary container with some text and a couple icons testing default fallbacks"
|
||||||
)
|
)
|
||||||
.size(24),
|
.size(24),
|
||||||
icon("microphone-sensitivity-high-symbolic-test", 24)
|
icon::handle::from_name("microphone-sensitivity-high-symbolic-test")
|
||||||
.style(cosmic::theme::Svg::SymbolicActive),
|
.size(24)
|
||||||
icon("microphone-sensitivity-high-symbolic-test", 16).default_fallbacks(false)
|
.icon(),
|
||||||
|
icon::handle::from_name("microphone-sensitivity-high-symbolic-test")
|
||||||
|
.size(24)
|
||||||
|
.fallback(false)
|
||||||
|
.icon(),
|
||||||
])
|
])
|
||||||
.layer(cosmic_theme::Layer::Primary)
|
.layer(cosmic_theme::Layer::Primary)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
|
|
@ -475,9 +447,7 @@ impl State {
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, c)| column![
|
.map(|(i, c)| column![
|
||||||
button(cosmic::theme::Button::Text)
|
button::text("Delete me").on_press(Message::DeleteCard(i)),
|
||||||
.text("Delete me")
|
|
||||||
.on_press(Message::DeleteCard(i)),
|
|
||||||
text(c).size(24).width(Length::Fill)
|
text(c).size(24).width(Length::Fill)
|
||||||
]
|
]
|
||||||
.into())
|
.into())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use cosmic::iced::widget::{horizontal_space, row};
|
use cosmic::iced::widget::{horizontal_space, row};
|
||||||
use cosmic::iced::{Alignment, Length};
|
use cosmic::iced::{Alignment, Length};
|
||||||
use cosmic::widget::{button, segmented_button, view_switcher};
|
use cosmic::widget::{button, icon, segmented_button, view_switcher};
|
||||||
use cosmic::{theme, Element};
|
use cosmic::{theme, Apply, Element};
|
||||||
use slotmap::Key;
|
use slotmap::Key;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
|
@ -66,8 +66,9 @@ impl State {
|
||||||
.on_close(Message::Close)
|
.on_close(Message::Close)
|
||||||
.width(Length::Shrink);
|
.width(Length::Shrink);
|
||||||
|
|
||||||
let new_tab_button = button(theme::Button::Text)
|
let new_tab_button = icon::handle::from_name("tab-new-symbolic")
|
||||||
.icon(theme::Svg::Symbolic, "tab-new-symbolic", 20)
|
.size(20)
|
||||||
|
.apply(button::icon)
|
||||||
.on_press(Message::AddNew);
|
.on_press(Message::AddNew);
|
||||||
|
|
||||||
let tab_header = row!(tabs, new_tab_button).align_items(Alignment::Center);
|
let tab_header = row!(tabs, new_tab_button).align_items(Alignment::Center);
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ impl State {
|
||||||
window.parent_page_button(SystemAndAccountsPage::About),
|
window.parent_page_button(SystemAndAccountsPage::About),
|
||||||
row!(
|
row!(
|
||||||
horizontal_space(Length::Fill),
|
horizontal_space(Length::Fill),
|
||||||
icon("distributor-logo", 78),
|
icon::handle::from_name("distributor-logo").size(78).icon(),
|
||||||
horizontal_space(Length::Fill),
|
horizontal_space(Length::Fill),
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use apply::Apply;
|
||||||
use cosmic::app::{Command, Core, Settings};
|
use cosmic::app::{Command, Core, Settings};
|
||||||
use cosmic::dialog::file_chooser::{self, FileFilter};
|
use cosmic::dialog::file_chooser::{self, FileFilter};
|
||||||
use cosmic::iced_core::Length;
|
use cosmic::iced_core::Length;
|
||||||
|
use cosmic::widget::button;
|
||||||
use cosmic::{executor, iced, ApplicationExt, Element};
|
use cosmic::{executor, iced, ApplicationExt, Element};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -82,10 +83,7 @@ impl cosmic::Application for App {
|
||||||
|
|
||||||
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
||||||
// Places a button the header to create open dialogs.
|
// Places a button the header to create open dialogs.
|
||||||
vec![cosmic::widget::button(cosmic::theme::Button::Primary)
|
vec![button::suggested("Open").on_press(Message::OpenFile).into()]
|
||||||
.text("Open")
|
|
||||||
.on_press(Message::OpenFile)
|
|
||||||
.into()]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {
|
fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {
|
||||||
|
|
|
||||||
2
iced
2
iced
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2ead0da06f6da58b01e107104808b45d6fb61e85
|
Subproject commit 8b2389f144966a5f9b60ab778c1073748fee5e70
|
||||||
6
res/external-link.svg
Normal file
6
res/external-link.svg
Normal 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 |
|
|
@ -127,7 +127,7 @@ impl CosmicAppletHelper {
|
||||||
pub fn icon_button<'a, Message: 'static>(
|
pub fn icon_button<'a, Message: 'static>(
|
||||||
&self,
|
&self,
|
||||||
icon_name: &'a str,
|
icon_name: &'a str,
|
||||||
) -> iced::widget::Button<'a, Message, Renderer> {
|
) -> crate::widget::Button<'a, Message, Renderer> {
|
||||||
crate::widget::button(theme::Button::Text)
|
crate::widget::button(theme::Button::Text)
|
||||||
.icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0)
|
.icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ where
|
||||||
} else {
|
} else {
|
||||||
theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance {
|
theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance {
|
||||||
background_color: iced_core::Color::TRANSPARENT,
|
background_color: iced_core::Color::TRANSPARENT,
|
||||||
|
icon_color: theme.cosmic().on_bg_color().into(),
|
||||||
text_color: theme.cosmic().on_bg_color().into(),
|
text_color: theme.cosmic().on_bg_color().into(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
151
src/app/mod.rs
151
src/app/mod.rs
|
|
@ -41,9 +41,9 @@ pub mod message {
|
||||||
pub use self::command::Command;
|
pub use self::command::Command;
|
||||||
pub use self::core::Core;
|
pub use self::core::Core;
|
||||||
pub use self::settings::Settings;
|
pub use self::settings::Settings;
|
||||||
|
use crate::prelude::*;
|
||||||
use crate::theme::THEME;
|
use crate::theme::THEME;
|
||||||
use crate::widget::nav_bar;
|
use crate::widget::nav_bar;
|
||||||
use crate::{Element, ElementExt};
|
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use iced::Subscription;
|
use iced::Subscription;
|
||||||
use iced::{window, Application as IcedApplication};
|
use iced::{window, Application as IcedApplication};
|
||||||
|
|
@ -261,91 +261,90 @@ impl<App: Application> ApplicationExt for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the view for the main window.
|
/// 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 core = self.core();
|
||||||
let is_condensed = core.is_condensed();
|
let is_condensed = core.is_condensed();
|
||||||
let mut main: Vec<Element<'a, Message<Self::Message>>> = Vec::with_capacity(2);
|
|
||||||
|
|
||||||
if core.window.show_headerbar {
|
crate::widget::column::with_capacity(2)
|
||||||
main.push({
|
.push_maybe(if core.window.show_headerbar {
|
||||||
let mut header = crate::widget::header_bar()
|
Some({
|
||||||
.title(self.title())
|
let mut header = crate::widget::header_bar()
|
||||||
.on_drag(Message::Cosmic(cosmic::Message::Drag))
|
.title(self.title())
|
||||||
.on_close(Message::Cosmic(cosmic::Message::Close));
|
.on_drag(Message::Cosmic(cosmic::Message::Drag))
|
||||||
|
.on_close(Message::Cosmic(cosmic::Message::Close));
|
||||||
|
|
||||||
if self.nav_model().is_some() {
|
if self.nav_model().is_some() {
|
||||||
let toggle = crate::widget::nav_bar_toggle()
|
let toggle = crate::widget::nav_bar_toggle()
|
||||||
.active(core.nav_bar_active())
|
.active(core.nav_bar_active())
|
||||||
.on_toggle(if is_condensed {
|
.on_toggle(if is_condensed {
|
||||||
Message::Cosmic(cosmic::Message::ToggleNavBarCondensed)
|
Message::Cosmic(cosmic::Message::ToggleNavBarCondensed)
|
||||||
} else {
|
} else {
|
||||||
Message::Cosmic(cosmic::Message::ToggleNavBar)
|
Message::Cosmic(cosmic::Message::ToggleNavBar)
|
||||||
});
|
});
|
||||||
|
|
||||||
header = header.start(toggle);
|
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));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if self.nav_model().is_none() || core.show_content() {
|
if core.window.show_maximize {
|
||||||
let main_content = self.view().debug(core.debug).map(Message::App);
|
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)
|
// The content element contains every element beneath the header.
|
||||||
.apply(iced::widget::container)
|
.push(
|
||||||
.padding([0, 8, 8, 8])
|
crate::widget::row::with_children({
|
||||||
.width(iced::Length::Fill)
|
let mut widgets = Vec::with_capacity(2);
|
||||||
.height(iced::Length::Fill)
|
|
||||||
.style(crate::theme::Container::Background)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use iced_core::Font;
|
||||||
|
|
||||||
/// Configure a new COSMIC application.
|
/// Configure a new COSMIC application.
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
#[must_use]
|
||||||
#[derive(derive_setters::Setters)]
|
#[derive(derive_setters::Setters)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
/// Produces a smoother result in some widgets, at a performance cost.
|
/// Produces a smoother result in some widgets, at a performance cost.
|
||||||
|
|
|
||||||
113
src/theme/button.rs
Normal file
113
src/theme/button.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/theme/mod.rs
144
src/theme/mod.rs
|
|
@ -4,17 +4,18 @@
|
||||||
//! Use COSMIC's themes and styles.
|
//! Use COSMIC's themes and styles.
|
||||||
|
|
||||||
pub mod expander;
|
pub mod expander;
|
||||||
|
|
||||||
|
mod button;
|
||||||
|
pub use self::button::Button;
|
||||||
|
|
||||||
mod segmented_button;
|
mod segmented_button;
|
||||||
|
pub use self::segmented_button::SegmentedButton;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
use std::hash::Hash;
|
|
||||||
use std::hash::Hasher;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use self::segmented_button::SegmentedButton;
|
|
||||||
|
|
||||||
use cosmic_config::config_subscription;
|
use cosmic_config::config_subscription;
|
||||||
use cosmic_config::CosmicConfigEntry;
|
use cosmic_config::CosmicConfigEntry;
|
||||||
use cosmic_theme::composite::over;
|
use cosmic_theme::composite::over;
|
||||||
|
|
@ -26,7 +27,7 @@ use iced_core::BorderRadius;
|
||||||
use iced_core::Radians;
|
use iced_core::Radians;
|
||||||
use iced_futures::Subscription;
|
use iced_futures::Subscription;
|
||||||
use iced_style::application;
|
use iced_style::application;
|
||||||
use iced_style::button;
|
use iced_style::button as iced_button;
|
||||||
use iced_style::checkbox;
|
use iced_style::checkbox;
|
||||||
use iced_style::container;
|
use iced_style::container;
|
||||||
use iced_style::menu;
|
use iced_style::menu;
|
||||||
|
|
@ -195,6 +196,7 @@ impl application::StyleSheet for Theme {
|
||||||
|
|
||||||
match style {
|
match style {
|
||||||
Application::Default => application::Appearance {
|
Application::Default => application::Appearance {
|
||||||
|
icon_color: cosmic.bg_color().into(),
|
||||||
background_color: cosmic.bg_color().into(),
|
background_color: cosmic.bg_color().into(),
|
||||||
text_color: cosmic.on_bg_color().into(),
|
text_color: cosmic.on_bg_color().into(),
|
||||||
},
|
},
|
||||||
|
|
@ -203,13 +205,13 @@ impl application::StyleSheet for Theme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/// Styles for the button widget from iced-rs.
|
||||||
* TODO: Button
|
#[derive(Default)]
|
||||||
*/
|
pub enum IcedButton {
|
||||||
pub enum Button {
|
|
||||||
Deactivated,
|
Deactivated,
|
||||||
Destructive,
|
Destructive,
|
||||||
Positive,
|
Positive,
|
||||||
|
#[default]
|
||||||
Primary,
|
Primary,
|
||||||
Secondary,
|
Secondary,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -218,110 +220,104 @@ pub enum Button {
|
||||||
Transparent,
|
Transparent,
|
||||||
Card,
|
Card,
|
||||||
Custom {
|
Custom {
|
||||||
active: Box<dyn Fn(&Theme) -> button::Appearance>,
|
active: Box<dyn Fn(&Theme) -> iced_button::Appearance>,
|
||||||
hover: Box<dyn Fn(&Theme) -> button::Appearance>,
|
hover: Box<dyn Fn(&Theme) -> iced_button::Appearance>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Button {
|
impl IcedButton {
|
||||||
fn default() -> Self {
|
|
||||||
Self::Primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Button {
|
|
||||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
#[allow(clippy::match_same_arms)]
|
#[allow(clippy::match_same_arms)]
|
||||||
fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent {
|
fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent {
|
||||||
let cosmic = theme.cosmic();
|
let cosmic = theme.cosmic();
|
||||||
match self {
|
match self {
|
||||||
Button::Primary => &cosmic.accent_button,
|
IcedButton::Primary => &cosmic.accent_button,
|
||||||
Button::Secondary => &theme.current_container().component,
|
IcedButton::Secondary => &theme.current_container().component,
|
||||||
Button::Positive => &cosmic.success_button,
|
IcedButton::Positive => &cosmic.success_button,
|
||||||
Button::Destructive => &cosmic.destructive_button,
|
IcedButton::Destructive => &cosmic.destructive_button,
|
||||||
Button::Text => &cosmic.text_button,
|
IcedButton::Text => &cosmic.text_button,
|
||||||
Button::Link => &cosmic.accent_button,
|
IcedButton::Link => &cosmic.accent_button,
|
||||||
Button::LinkActive => &cosmic.accent_button,
|
IcedButton::LinkActive => &cosmic.accent_button,
|
||||||
Button::Transparent => &TRANSPARENT_COMPONENT,
|
IcedButton::Transparent => &TRANSPARENT_COMPONENT,
|
||||||
Button::Deactivated => &theme.current_container().component,
|
IcedButton::Deactivated => &theme.current_container().component,
|
||||||
Button::Card => &theme.current_container().component,
|
IcedButton::Card => &theme.current_container().component,
|
||||||
Button::Custom { .. } => &TRANSPARENT_COMPONENT,
|
IcedButton::Custom { .. } => &TRANSPARENT_COMPONENT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl button::StyleSheet for Theme {
|
impl iced_button::StyleSheet for Theme {
|
||||||
type Style = Button;
|
type Style = IcedButton;
|
||||||
|
|
||||||
fn active(&self, style: &Self::Style) -> button::Appearance {
|
fn active(&self, style: &Self::Style) -> iced_button::Appearance {
|
||||||
if let Button::Custom { active, .. } = style {
|
if let IcedButton::Custom { active, .. } = style {
|
||||||
return active(self);
|
return active(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
let corner_radii = &self.cosmic().corner_radii;
|
let corner_radii = &self.cosmic().corner_radii;
|
||||||
let component = style.cosmic(self);
|
let component = style.cosmic(self);
|
||||||
button::Appearance {
|
iced_button::Appearance {
|
||||||
border_radius: match style {
|
border_radius: match style {
|
||||||
Button::Link => corner_radii.radius_0.into(),
|
IcedButton::Link => corner_radii.radius_0.into(),
|
||||||
Button::Card => corner_radii.radius_xs.into(),
|
IcedButton::Card => corner_radii.radius_xs.into(),
|
||||||
_ => corner_radii.radius_xl.into(),
|
_ => corner_radii.radius_xl.into(),
|
||||||
},
|
},
|
||||||
background: match style {
|
background: match style {
|
||||||
Button::Link | Button::Text => None,
|
IcedButton::Link | IcedButton::Text => None,
|
||||||
Button::LinkActive => Some(Background::Color(component.divider.into())),
|
IcedButton::LinkActive => Some(Background::Color(component.divider.into())),
|
||||||
_ => Some(Background::Color(component.base.into())),
|
_ => Some(Background::Color(component.base.into())),
|
||||||
},
|
},
|
||||||
text_color: match style {
|
text_color: match style {
|
||||||
Button::Link | Button::LinkActive => component.base.into(),
|
IcedButton::Link | IcedButton::LinkActive => component.base.into(),
|
||||||
_ => component.on.into(),
|
_ => component.on.into(),
|
||||||
},
|
},
|
||||||
..button::Appearance::default()
|
..iced_button::Appearance::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hovered(&self, style: &Self::Style) -> button::Appearance {
|
fn hovered(&self, style: &Self::Style) -> iced_button::Appearance {
|
||||||
if let Button::Custom { hover, .. } = style {
|
if let IcedButton::Custom { hover, .. } = style {
|
||||||
return hover(self);
|
return hover(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
let active = self.active(style);
|
let active = self.active(style);
|
||||||
let component = style.cosmic(self);
|
let component = style.cosmic(self);
|
||||||
|
|
||||||
button::Appearance {
|
iced_button::Appearance {
|
||||||
background: match style {
|
background: match style {
|
||||||
Button::Link => None,
|
IcedButton::Link => None,
|
||||||
Button::LinkActive => Some(Background::Color(component.divider.into())),
|
IcedButton::LinkActive => Some(Background::Color(component.divider.into())),
|
||||||
_ => Some(Background::Color(component.hover.into())),
|
_ => Some(Background::Color(component.hover.into())),
|
||||||
},
|
},
|
||||||
..active
|
..active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focused(&self, style: &Self::Style) -> button::Appearance {
|
fn focused(&self, style: &Self::Style) -> iced_button::Appearance {
|
||||||
if let Button::Custom { hover, .. } = style {
|
if let IcedButton::Custom { hover, .. } = style {
|
||||||
return hover(self);
|
return hover(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
let active = self.active(style);
|
let active = self.active(style);
|
||||||
let component = style.cosmic(self);
|
let component = style.cosmic(self);
|
||||||
button::Appearance {
|
iced_button::Appearance {
|
||||||
background: match style {
|
background: match style {
|
||||||
Button::Link => None,
|
IcedButton::Link => None,
|
||||||
Button::LinkActive => Some(Background::Color(component.divider.into())),
|
IcedButton::LinkActive => Some(Background::Color(component.divider.into())),
|
||||||
_ => Some(Background::Color(component.hover.into())),
|
_ => Some(Background::Color(component.hover.into())),
|
||||||
},
|
},
|
||||||
..active
|
..active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disabled(&self, style: &Self::Style) -> button::Appearance {
|
fn disabled(&self, style: &Self::Style) -> iced_button::Appearance {
|
||||||
let active = self.active(style);
|
let active = self.active(style);
|
||||||
|
|
||||||
if matches!(style, Button::Card) {
|
if matches!(style, IcedButton::Card) {
|
||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|
||||||
button::Appearance {
|
iced_button::Appearance {
|
||||||
shadow_offset: iced_core::Vector::default(),
|
shadow_offset: iced_core::Vector::default(),
|
||||||
background: active.background.map(|background| match background {
|
background: active.background.map(|background| match background {
|
||||||
Background::Color(color) => Background::Color(Color {
|
Background::Color(color) => Background::Color(Color {
|
||||||
|
|
@ -563,6 +559,7 @@ impl container::StyleSheet for Theme {
|
||||||
let palette = self.cosmic();
|
let palette = self.cosmic();
|
||||||
|
|
||||||
container::Appearance {
|
container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.background.on)),
|
||||||
text_color: Some(Color::from(palette.background.on)),
|
text_color: Some(Color::from(palette.background.on)),
|
||||||
background: Some(iced::Background::Color(palette.background.base.into())),
|
background: Some(iced::Background::Color(palette.background.base.into())),
|
||||||
border_radius: 2.0.into(),
|
border_radius: 2.0.into(),
|
||||||
|
|
@ -577,6 +574,7 @@ impl container::StyleSheet for Theme {
|
||||||
header_top.alpha = 0.8;
|
header_top.alpha = 0.8;
|
||||||
|
|
||||||
container::Appearance {
|
container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.accent.base)),
|
||||||
text_color: Some(Color::from(palette.background.on)),
|
text_color: Some(Color::from(palette.background.on)),
|
||||||
background: Some(iced::Background::Gradient(iced_core::Gradient::Linear(
|
background: Some(iced::Background::Gradient(iced_core::Gradient::Linear(
|
||||||
Linear::new(Radians(3.0 * PI / 2.0))
|
Linear::new(Radians(3.0 * PI / 2.0))
|
||||||
|
|
@ -592,6 +590,7 @@ impl container::StyleSheet for Theme {
|
||||||
let palette = self.cosmic();
|
let palette = self.cosmic();
|
||||||
|
|
||||||
container::Appearance {
|
container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.primary.on)),
|
||||||
text_color: Some(Color::from(palette.primary.on)),
|
text_color: Some(Color::from(palette.primary.on)),
|
||||||
background: Some(iced::Background::Color(palette.primary.base.into())),
|
background: Some(iced::Background::Color(palette.primary.base.into())),
|
||||||
border_radius: 2.0.into(),
|
border_radius: 2.0.into(),
|
||||||
|
|
@ -603,6 +602,7 @@ impl container::StyleSheet for Theme {
|
||||||
let palette = self.cosmic();
|
let palette = self.cosmic();
|
||||||
|
|
||||||
container::Appearance {
|
container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.secondary.on)),
|
||||||
text_color: Some(Color::from(palette.secondary.on)),
|
text_color: Some(Color::from(palette.secondary.on)),
|
||||||
background: Some(iced::Background::Color(palette.secondary.base.into())),
|
background: Some(iced::Background::Color(palette.secondary.base.into())),
|
||||||
border_radius: 2.0.into(),
|
border_radius: 2.0.into(),
|
||||||
|
|
@ -615,6 +615,7 @@ impl container::StyleSheet for Theme {
|
||||||
|
|
||||||
match self.layer {
|
match self.layer {
|
||||||
cosmic_theme::Layer::Background => container::Appearance {
|
cosmic_theme::Layer::Background => container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.background.component.on)),
|
||||||
text_color: Some(Color::from(palette.background.component.on)),
|
text_color: Some(Color::from(palette.background.component.on)),
|
||||||
background: Some(iced::Background::Color(
|
background: Some(iced::Background::Color(
|
||||||
palette.background.component.base.into(),
|
palette.background.component.base.into(),
|
||||||
|
|
@ -624,6 +625,7 @@ impl container::StyleSheet for Theme {
|
||||||
border_color: Color::TRANSPARENT,
|
border_color: Color::TRANSPARENT,
|
||||||
},
|
},
|
||||||
cosmic_theme::Layer::Primary => container::Appearance {
|
cosmic_theme::Layer::Primary => container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.primary.component.on)),
|
||||||
text_color: Some(Color::from(palette.primary.component.on)),
|
text_color: Some(Color::from(palette.primary.component.on)),
|
||||||
background: Some(iced::Background::Color(
|
background: Some(iced::Background::Color(
|
||||||
palette.primary.component.base.into(),
|
palette.primary.component.base.into(),
|
||||||
|
|
@ -633,6 +635,7 @@ impl container::StyleSheet for Theme {
|
||||||
border_color: Color::TRANSPARENT,
|
border_color: Color::TRANSPARENT,
|
||||||
},
|
},
|
||||||
cosmic_theme::Layer::Secondary => container::Appearance {
|
cosmic_theme::Layer::Secondary => container::Appearance {
|
||||||
|
icon_color: Some(Color::from(palette.secondary.component.on)),
|
||||||
text_color: Some(Color::from(palette.secondary.component.on)),
|
text_color: Some(Color::from(palette.secondary.component.on)),
|
||||||
background: Some(iced::Background::Color(
|
background: Some(iced::Background::Color(
|
||||||
palette.secondary.component.base.into(),
|
palette.secondary.component.base.into(),
|
||||||
|
|
@ -1021,29 +1024,6 @@ pub enum Svg {
|
||||||
/// No filtering is applied
|
/// No filtering is applied
|
||||||
#[default]
|
#[default]
|
||||||
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 {
|
impl Svg {
|
||||||
|
|
@ -1060,18 +1040,6 @@ impl svg::StyleSheet for Theme {
|
||||||
match style {
|
match style {
|
||||||
Svg::Default => svg::Appearance::default(),
|
Svg::Default => svg::Appearance::default(),
|
||||||
Svg::Custom(appearance) => appearance(self),
|
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()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
src/widget/button/hyperlink.rs
Normal file
50
src/widget/button/hyperlink.rs
Normal 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
179
src/widget/button/icon.rs
Normal 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
113
src/widget/button/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/widget/button/style.rs
Normal file
90
src/widget/button/style.rs
Normal 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
123
src/widget/button/text.rs
Normal 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
677
src/widget/button/widget.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
pub mod style;
|
pub mod style;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use iced_core::{Background, Color};
|
use iced_core::{Background, Color};
|
||||||
|
|
||||||
/// Appearance of the cards.
|
/// Appearance of the cards.
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use crate::ext::CollectionWidget;
|
||||||
|
use crate::widget::{column, row};
|
||||||
use crate::Element;
|
use crate::Element;
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::widget::{column, row};
|
|
||||||
use iced_core::{alignment, Length, Size};
|
use iced_core::{alignment, Length, Size};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
/// Responsively generates rows and columns of widgets based on its dimmensions.
|
/// Responsively generates rows and columns of widgets based on its dimmensions.
|
||||||
#[derive(Setters)]
|
#[derive(Setters)]
|
||||||
pub struct FlexRow<'a, Message> {
|
pub struct FlexRow<'a, Message> {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
generator: Box<dyn Fn(&mut Vec<Element<'a, Message>>, Size) -> u16 + 'a>,
|
generator: Box<dyn Fn(&mut Vec<Element<'a, Message>>, Size) -> u16 + 'a>,
|
||||||
/// Sets the space between each column of items.
|
/// 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(..);
|
let mut iterator = elements.drain(..);
|
||||||
|
|
||||||
while let Some(element) = iterator.next() {
|
while let Some(element) = iterator.next() {
|
||||||
let mut elements_row = Vec::with_capacity(items_per_row);
|
let elements_row = row::with_capacity(items_per_row)
|
||||||
elements_row.push(element);
|
.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_column.push(elements_row.into());
|
||||||
elements_row.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
elements_column.push(row(elements_row).spacing(container.row_spacing).into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
column(elements_column)
|
column::with_children(elements_column)
|
||||||
.spacing(container.column_spacing)
|
.spacing(container.column_spacing)
|
||||||
.apply(iced::widget::container)
|
.apply(iced::widget::container)
|
||||||
.align_x(container.align_x)
|
.align_x(container.align_x)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::{theme, Element};
|
use crate::{ext::CollectionWidget, widget, Element};
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::{self, widget, Length};
|
use iced::Length;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -89,51 +89,43 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
/// Converts the headerbar builder into an Iced element.
|
/// Converts the headerbar builder into an Iced element.
|
||||||
pub fn into_element(mut self) -> Element<'a, Message> {
|
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.
|
// Take ownership of the regions to be packed.
|
||||||
let start = std::mem::take(&mut self.start);
|
let start = std::mem::take(&mut self.start);
|
||||||
let center = std::mem::take(&mut self.center);
|
let center = std::mem::take(&mut self.center);
|
||||||
let mut end = std::mem::take(&mut self.end);
|
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.
|
// 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());
|
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.
|
// 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))
|
.height(Length::Fixed(50.0))
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
|
|
@ -159,7 +151,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
let mut title = Cow::default();
|
let mut title = Cow::default();
|
||||||
std::mem::swap(&mut title, &mut self.title);
|
std::mem::swap(&mut title, &mut self.title);
|
||||||
|
|
||||||
super::text(title)
|
widget::text(title)
|
||||||
.size(16)
|
.size(16)
|
||||||
.font(crate::font::FONT_SEMIBOLD)
|
.font(crate::font::FONT_SEMIBOLD)
|
||||||
.apply(widget::container)
|
.apply(widget::container)
|
||||||
|
|
@ -172,30 +164,30 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
|
|
||||||
/// Creates the widget for window controls.
|
/// Creates the widget for window controls.
|
||||||
fn window_controls(&mut self) -> Element<'a, Message> {
|
fn window_controls(&mut self) -> Element<'a, Message> {
|
||||||
let mut widgets: Vec<Element<_>> = Vec::with_capacity(3);
|
|
||||||
|
|
||||||
let icon = |name, size, on_press| {
|
let icon = |name, size, on_press| {
|
||||||
super::icon(name, size)
|
widget::icon::handle::from_name(name)
|
||||||
.force_svg(true)
|
.size(size)
|
||||||
.style(crate::theme::Svg::SymbolicActive)
|
.handle()
|
||||||
.apply(widget::button)
|
.apply(widget::button::icon)
|
||||||
.style(theme::Button::Text)
|
|
||||||
.on_press(on_press)
|
.on_press(on_press)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(message) = self.on_minimize.take() {
|
widget::row::with_capacity(3)
|
||||||
widgets.push(icon("window-minimize-symbolic", 16, message).into());
|
.push_maybe(
|
||||||
}
|
self.on_minimize
|
||||||
|
.take()
|
||||||
if let Some(message) = self.on_maximize.take() {
|
.map(|m| icon("window-minimize-symbolic", 16, m)),
|
||||||
widgets.push(icon("window-maximize-symbolic", 16, message).into());
|
)
|
||||||
}
|
.push_maybe(
|
||||||
|
self.on_maximize
|
||||||
if let Some(message) = self.on_close.take() {
|
.take()
|
||||||
widgets.push(icon("window-close-symbolic", 16, message).into());
|
.map(|m| icon("window-maximize-symbolic", 16, m)),
|
||||||
}
|
)
|
||||||
|
.push_maybe(
|
||||||
widget::row(widgets)
|
self.on_close
|
||||||
|
.take()
|
||||||
|
.map(|m| icon("window-close-symbolic", 16, m)),
|
||||||
|
)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.apply(widget::container)
|
.apply(widget::container)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
|
||||||
|
|
@ -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
114
src/widget/icon/builder.rs
Normal 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
88
src/widget/icon/handle.rs
Normal 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
104
src/widget/icon/mod.rs
Normal 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>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,9 +41,9 @@ impl<'a, Message: 'static> ListColumn<'a, Message> {
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_element(self) -> Element<'a, Message> {
|
pub fn into_element(self) -> Element<'a, Message> {
|
||||||
iced::widget::column(self.children)
|
crate::widget::column::with_children(self.children)
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.apply(iced::widget::container)
|
.apply(crate::widget::container)
|
||||||
.padding([16, 6])
|
.padding([16, 6])
|
||||||
.style(theme::Container::custom(style))
|
.style(theme::Container::custom(style))
|
||||||
.into()
|
.into()
|
||||||
|
|
@ -58,9 +58,10 @@ impl<'a, Message: 'static> From<ListColumn<'a, Message>> for Element<'a, Message
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
#[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;
|
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()),
|
text_color: Some(container.on.into()),
|
||||||
background: Some(Background::Color(container.base.into())),
|
background: Some(Background::Color(container.base.into())),
|
||||||
border_radius: 8.0.into(),
|
border_radius: 8.0.into(),
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ pub use iced::widget::{svg, Svg};
|
||||||
|
|
||||||
pub mod aspect_ratio;
|
pub mod aspect_ratio;
|
||||||
|
|
||||||
mod button;
|
pub mod button;
|
||||||
pub use button::*;
|
pub use button::{button, Button, IconButton, LinkButton, TextButton};
|
||||||
|
|
||||||
pub mod card;
|
pub mod card;
|
||||||
pub use card::*;
|
pub use card::*;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ where
|
||||||
.button_height(32)
|
.button_height(32)
|
||||||
.button_padding([16, 10, 16, 10])
|
.button_padding([16, 10, 16, 10])
|
||||||
.button_spacing(8)
|
.button_spacing(8)
|
||||||
.icon_size(16)
|
|
||||||
.on_activate(on_activate)
|
.on_activate(on_activate)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
.style(crate::theme::SegmentedButton::ViewSwitcher)
|
||||||
|
|
@ -46,6 +45,7 @@ where
|
||||||
pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance {
|
pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance {
|
||||||
let cosmic = &theme.cosmic();
|
let cosmic = &theme.cosmic();
|
||||||
iced_style::container::Appearance {
|
iced_style::container::Appearance {
|
||||||
|
icon_color: Some(cosmic.on_bg_color().into()),
|
||||||
text_color: Some(cosmic.on_bg_color().into()),
|
text_color: Some(cosmic.on_bg_color().into()),
|
||||||
background: Some(Background::Color(cosmic.primary.base.into())),
|
background: Some(Background::Color(cosmic.primary.base.into())),
|
||||||
border_radius: 8.0.into(),
|
border_radius: 8.0.into(),
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
|
|
||||||
//! A button for toggling the navigation side panel.
|
//! A button for toggling the navigation side panel.
|
||||||
|
|
||||||
use crate::{theme, Element};
|
use crate::{widget, Element};
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::Length;
|
use iced::Length;
|
||||||
|
|
||||||
use super::IconSource;
|
|
||||||
|
|
||||||
#[derive(Setters)]
|
#[derive(Setters)]
|
||||||
pub struct NavBarToggle<Message> {
|
pub struct NavBarToggle<Message> {
|
||||||
active: bool,
|
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> {
|
impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, Message> {
|
||||||
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
|
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
|
||||||
let mut widget = super::icon(
|
let icon = if nav_bar_toggle.active {
|
||||||
if nav_bar_toggle.active {
|
widget::icon::handle::from_svg_bytes(
|
||||||
IconSource::svg_from_memory(&include_bytes!("../../res/sidebar-active.svg")[..])
|
&include_bytes!("../../res/sidebar-active.svg")[..],
|
||||||
} else {
|
)
|
||||||
IconSource::from("open-menu-symbolic")
|
.symbolic(true)
|
||||||
},
|
} else {
|
||||||
16,
|
widget::icon::handle::from_name("open-menu-symbolic")
|
||||||
)
|
.size(16)
|
||||||
.style(theme::Svg::SymbolicActive)
|
.handle()
|
||||||
.apply(iced::widget::container)
|
};
|
||||||
.apply(iced::widget::button)
|
|
||||||
.padding([8, 16, 8, 16])
|
|
||||||
.style(theme::Button::Text);
|
|
||||||
|
|
||||||
if let Some(message) = nav_bar_toggle.on_toggle {
|
widget::button::text("")
|
||||||
widget = widget.on_press(message);
|
.leading_icon(icon)
|
||||||
}
|
.padding([8, 16, 8, 16])
|
||||||
|
.on_press_maybe(nav_bar_toggle.on_toggle)
|
||||||
widget
|
.apply(widget::container)
|
||||||
.apply(iced::widget::container)
|
|
||||||
.center_y()
|
.center_y()
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::iced::{
|
use crate::iced::{Background, Length};
|
||||||
self,
|
use crate::widget::{container, icon, row, text_input};
|
||||||
widget::{container, Button},
|
|
||||||
Background, Length,
|
|
||||||
};
|
|
||||||
use crate::Renderer;
|
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
|
|
||||||
/// A search field for COSMIC applications.
|
/// 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> {
|
impl<'a, Message: 'static + Clone> Field<'a, Message> {
|
||||||
pub fn into_element(mut self) -> crate::Element<'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)
|
.on_input(self.on_change)
|
||||||
.style(crate::theme::TextInput::Search)
|
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.id(self.id);
|
.id(self.id)
|
||||||
|
.on_submit_maybe(self.on_submit.take());
|
||||||
|
|
||||||
if let Some(message) = self.on_submit.take() {
|
row::with_capacity(3)
|
||||||
input = input.on_submit(message);
|
.push(
|
||||||
}
|
icon::handle::from_svg_bytes(&include_bytes!("search.svg")[..])
|
||||||
|
.symbolic(true)
|
||||||
iced::widget::row!(
|
.icon()
|
||||||
super::icon::search(16),
|
.size(16),
|
||||||
input,
|
)
|
||||||
clear_button().on_press(self.on_clear)
|
.push(input)
|
||||||
)
|
.push(clear_button().on_press(self.on_clear))
|
||||||
.width(Length::Fixed(300.0))
|
.width(Length::Fixed(300.0))
|
||||||
.height(Length::Fixed(38.0))
|
.height(Length::Fixed(38.0))
|
||||||
.padding([0, 16])
|
.padding([0, 16])
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_items(iced::Alignment::Center)
|
.align_items(iced::Alignment::Center)
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.style(crate::theme::Container::custom(active_style))
|
.style(crate::theme::Container::custom(active_style))
|
||||||
.into()
|
.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> {
|
fn clear_button<Message: 'static>() -> crate::widget::IconButton<'static, Message> {
|
||||||
super::icon::edit_clear(16)
|
icon::handle::from_name("edit-clear-symbolic")
|
||||||
.style(crate::theme::Svg::Symbolic)
|
.size(16)
|
||||||
.apply(iced::widget::button)
|
.apply(crate::widget::button::icon)
|
||||||
.style(crate::theme::Button::Text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
#[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;
|
let mut neutral_7 = cosmic.palette.neutral_7;
|
||||||
neutral_7.alpha = 0.25;
|
neutral_7.alpha = 0.25;
|
||||||
iced::widget::container::Appearance {
|
iced::widget::container::Appearance {
|
||||||
|
icon_color: Some(cosmic.palette.neutral_9.into()),
|
||||||
text_color: Some(cosmic.palette.neutral_9.into()),
|
text_color: Some(cosmic.palette.neutral_9.into()),
|
||||||
background: Some(Background::Color(neutral_7.into())),
|
background: Some(Background::Color(neutral_7.into())),
|
||||||
border_radius: 24.0.into(),
|
border_radius: 24.0.into(),
|
||||||
|
|
|
||||||
|
|
@ -43,16 +43,15 @@ mod field;
|
||||||
mod model;
|
mod model;
|
||||||
|
|
||||||
mod button {
|
mod button {
|
||||||
use crate::iced::{self, widget::container};
|
use crate::widget::{container, icon};
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
|
|
||||||
/// A search button which converts to a search [`field`] on click.
|
/// A search button which converts to a search [`field`] on click.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn button<Message: 'static + Clone>(on_press: Message) -> crate::Element<'static, Message> {
|
pub fn button<Message: 'static + Clone>(on_press: Message) -> crate::Element<'static, Message> {
|
||||||
super::icon::search(16)
|
icon::handle::from_svg_bytes(&include_bytes!("search.svg")[..])
|
||||||
.style(crate::theme::Svg::SymbolicActive)
|
.symbolic(true)
|
||||||
.apply(iced::widget::button)
|
.apply(crate::widget::button::icon)
|
||||||
.style(crate::theme::Button::Text)
|
|
||||||
.on_press(on_press)
|
.on_press(on_press)
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.padding([0, 0, 0, 11])
|
.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 button::button;
|
||||||
pub use field::{field, Field};
|
pub use field::{field, Field};
|
||||||
pub use model::Model;
|
pub use model::Model;
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ use iced::{Length, Rectangle, Size};
|
||||||
use iced_core::layout;
|
use iced_core::layout;
|
||||||
|
|
||||||
/// Horizontal [`SegmentedButton`].
|
/// Horizontal [`SegmentedButton`].
|
||||||
pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> =
|
pub type HorizontalSegmentedButton<'a, SelectionMode, Message> =
|
||||||
SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>;
|
SegmentedButton<'a, Horizontal, SelectionMode, Message>;
|
||||||
|
|
||||||
/// A type marker defining the horizontal variant of a [`SegmentedButton`].
|
/// A type marker defining the horizontal variant of a [`SegmentedButton`].
|
||||||
pub struct Horizontal;
|
pub struct Horizontal;
|
||||||
|
|
@ -21,36 +21,24 @@ pub struct Horizontal;
|
||||||
///
|
///
|
||||||
/// For details on the model, see the [`segmented_button`](super) module for more details.
|
/// For details on the model, see the [`segmented_button`](super) module for more details.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn horizontal<SelectionMode: Default, Message, Renderer>(
|
pub fn horizontal<SelectionMode: Default, Message>(
|
||||||
model: &Model<SelectionMode>,
|
model: &Model<SelectionMode>,
|
||||||
) -> SegmentedButton<Horizontal, SelectionMode, Message, Renderer>
|
) -> SegmentedButton<Horizontal, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
{
|
{
|
||||||
SegmentedButton::new(model)
|
SegmentedButton::new(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, SelectionMode, Message, Renderer> SegmentedVariant
|
impl<'a, SelectionMode, Message> SegmentedVariant
|
||||||
for SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>
|
for SegmentedButton<'a, Horizontal, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
{
|
{
|
||||||
type Renderer = Renderer;
|
|
||||||
|
|
||||||
fn variant_appearance(
|
fn variant_appearance(
|
||||||
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
theme: &crate::Theme,
|
||||||
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
style: &crate::theme::SegmentedButton,
|
||||||
) -> super::Appearance {
|
) -> super::Appearance {
|
||||||
theme.horizontal(style)
|
theme.horizontal(style)
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +61,7 @@ where
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
#[allow(clippy::cast_sign_loss)]
|
#[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 limits = limits.width(self.width);
|
||||||
let (mut width, height) = self.max_button_dimensions(renderer, limits.max());
|
let (mut width, height) = self.max_button_dimensions(renderer, limits.max());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,3 @@ pub type SecondaryMap<T> = slotmap::SecondaryMap<Entity, T>;
|
||||||
///
|
///
|
||||||
/// Sparse maps internally use a `HashMap`, for data that is sparsely associated.
|
/// Sparse maps internally use a `HashMap`, for data that is sparsely associated.
|
||||||
pub type SparseSecondaryMap<T> = slotmap::SparseSecondaryMap<Entity, T>;
|
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),
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use iced::Color;
|
|
||||||
use slotmap::{SecondaryMap, SparseSecondaryMap};
|
use slotmap::{SecondaryMap, SparseSecondaryMap};
|
||||||
|
|
||||||
use super::{Entity, Model, Selectable};
|
use super::{Entity, Model, Selectable};
|
||||||
use crate::widget::IconSource;
|
use crate::widget::icon::Icon;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// A builder for a [`Model`].
|
/// A builder for a [`Model`].
|
||||||
|
|
@ -104,18 +103,11 @@ where
|
||||||
/// .build()
|
/// .build()
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
#[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.model.0.icon_set(self.id, icon);
|
||||||
self
|
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.
|
/// Define the position of the newly-inserted item.
|
||||||
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
||||||
pub fn position(mut self, position: u16) -> Self {
|
pub fn position(mut self, position: u16) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,9 @@
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use iced::Color;
|
|
||||||
use slotmap::{SecondaryMap, SparseSecondaryMap};
|
use slotmap::{SecondaryMap, SparseSecondaryMap};
|
||||||
|
|
||||||
use crate::widget::IconSource;
|
use crate::widget::Icon;
|
||||||
|
|
||||||
use super::{Entity, Model, Selectable};
|
use super::{Entity, Model, Selectable};
|
||||||
|
|
||||||
|
|
@ -90,18 +89,11 @@ where
|
||||||
/// model.insert().text("Item A").icon(IconSource::from("icon-a"));
|
/// model.insert().text("Item A").icon(IconSource::from("icon-a"));
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
#[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.model.icon_set(self.id, icon);
|
||||||
self
|
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.
|
/// Returns the ID of the item that was inserted.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,12 @@ pub use self::entity::EntityMut;
|
||||||
mod selection;
|
mod selection;
|
||||||
pub use self::selection::{MultiSelect, Selectable, SingleSelect};
|
pub use self::selection::{MultiSelect, Selectable, SingleSelect};
|
||||||
|
|
||||||
use crate::widget::IconSource;
|
use crate::widget::Icon;
|
||||||
use iced::Color;
|
|
||||||
use slotmap::{SecondaryMap, SlotMap};
|
use slotmap::{SecondaryMap, SlotMap};
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use super::IconColor;
|
|
||||||
|
|
||||||
slotmap::new_key_type! {
|
slotmap::new_key_type! {
|
||||||
/// A unique ID for an item in the [`Model`].
|
/// A unique ID for an item in the [`Model`].
|
||||||
pub struct Entity;
|
pub struct Entity;
|
||||||
|
|
@ -62,7 +59,7 @@ pub struct Model<SelectionMode: Default> {
|
||||||
pub(super) items: SlotMap<Entity, Settings>,
|
pub(super) items: SlotMap<Entity, Settings>,
|
||||||
|
|
||||||
/// Icons optionally-defined for each item.
|
/// 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.
|
/// Text optionally-defined for each item.
|
||||||
pub(super) text: SecondaryMap<Entity, Cow<'static, str>>,
|
pub(super) text: SecondaryMap<Entity, Cow<'static, str>>,
|
||||||
|
|
@ -224,7 +221,7 @@ where
|
||||||
/// println!("has icon: {:?}", icon);
|
/// 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)
|
self.icons.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,34 +232,12 @@ where
|
||||||
/// println!("previously had icon: {:?}", old_icon);
|
/// println!("previously had icon: {:?}", old_icon);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn icon_set(
|
pub fn icon_set(&mut self, id: Entity, icon: Icon) -> Option<Icon> {
|
||||||
&mut self,
|
|
||||||
id: Entity,
|
|
||||||
icon: impl Into<IconSource<'static>>,
|
|
||||||
) -> Option<IconSource<'static>> {
|
|
||||||
if !self.contains_item(id) {
|
if !self.contains_item(id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.icons.insert(id, icon.into())
|
self.icons.insert(id, icon)
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the icon from an item.
|
/// Removes the icon from an item.
|
||||||
|
|
@ -271,7 +246,7 @@ where
|
||||||
/// if let Some(old_icon) = model.icon_remove(id) {
|
/// if let Some(old_icon) = model.icon_remove(id) {
|
||||||
/// println!("previously had icon: {:?}", old_icon);
|
/// 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)
|
self.icons.remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,44 +14,32 @@ use iced_core::layout;
|
||||||
pub struct Vertical;
|
pub struct Vertical;
|
||||||
|
|
||||||
/// Vertical [`SegmentedButton`].
|
/// Vertical [`SegmentedButton`].
|
||||||
pub type VerticalSegmentedButton<'a, SelectionMode, Message, Renderer> =
|
pub type VerticalSegmentedButton<'a, SelectionMode, Message> =
|
||||||
SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>;
|
SegmentedButton<'a, Vertical, SelectionMode, Message>;
|
||||||
|
|
||||||
/// Vertical implementation of the [`SegmentedButton`].
|
/// Vertical implementation of the [`SegmentedButton`].
|
||||||
///
|
///
|
||||||
/// For details on the model, see the [`segmented_button`](super) module for more details.
|
/// For details on the model, see the [`segmented_button`](super) module for more details.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn vertical<SelectionMode, Message, Renderer>(
|
pub fn vertical<SelectionMode, Message>(
|
||||||
model: &Model<SelectionMode>,
|
model: &Model<SelectionMode>,
|
||||||
) -> SegmentedButton<Vertical, SelectionMode, Message, Renderer>
|
) -> SegmentedButton<Vertical, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
{
|
{
|
||||||
SegmentedButton::new(model)
|
SegmentedButton::new(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, SelectionMode, Message, Renderer> SegmentedVariant
|
impl<'a, SelectionMode, Message> SegmentedVariant
|
||||||
for SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>
|
for SegmentedButton<'a, Vertical, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
{
|
{
|
||||||
type Renderer = Renderer;
|
|
||||||
|
|
||||||
fn variant_appearance(
|
fn variant_appearance(
|
||||||
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
theme: &crate::Theme,
|
||||||
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
style: &crate::theme::SegmentedButton,
|
||||||
) -> super::Appearance {
|
) -> super::Appearance {
|
||||||
theme.vertical(style)
|
theme.vertical(style)
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +62,7 @@ where
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
#[allow(clippy::cast_sign_loss)]
|
#[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 limits = limits.width(self.width);
|
||||||
let (width, mut height) = self.max_button_dimensions(renderer, limits.max());
|
let (width, mut height) = self.max_button_dimensions(renderer, limits.max());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use super::model::{Entity, Model, Selectable};
|
use super::model::{Entity, Model, Selectable};
|
||||||
use super::style::StyleSheet;
|
use crate::theme::SegmentedButton as Style;
|
||||||
use super::IconColor;
|
use crate::widget::{icon, Icon};
|
||||||
use crate::widget::{icon, IconSource};
|
use crate::{Element, Renderer};
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::{
|
use iced::{
|
||||||
alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length,
|
alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle,
|
||||||
Rectangle, Size,
|
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::widget::{self, operation, tree};
|
||||||
use iced_core::BorderRadius;
|
|
||||||
use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
|
||||||
|
use iced_core::{BorderRadius, Point, Renderer as IcedRenderer};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
/// State that is maintained by each individual widget.
|
/// State that is maintained by each individual widget.
|
||||||
|
|
@ -47,32 +47,23 @@ impl operation::Focusable for LocalState {
|
||||||
|
|
||||||
/// Isolates variant-specific behaviors from [`SegmentedButton`].
|
/// Isolates variant-specific behaviors from [`SegmentedButton`].
|
||||||
pub trait SegmentedVariant {
|
pub trait SegmentedVariant {
|
||||||
type Renderer: iced_core::Renderer;
|
|
||||||
|
|
||||||
/// Get the appearance for this variant of the widget.
|
/// Get the appearance for this variant of the widget.
|
||||||
fn variant_appearance(
|
fn variant_appearance(
|
||||||
theme: &<Self::Renderer as iced_core::Renderer>::Theme,
|
theme: &crate::Theme,
|
||||||
style: &<<Self::Renderer as iced_core::Renderer>::Theme as StyleSheet>::Style,
|
style: &crate::theme::SegmentedButton,
|
||||||
) -> super::Appearance
|
) -> super::Appearance;
|
||||||
where
|
|
||||||
<Self::Renderer as iced_core::Renderer>::Theme: StyleSheet;
|
|
||||||
|
|
||||||
/// Calculates the bounds for the given button by its position.
|
/// Calculates the bounds for the given button by its position.
|
||||||
fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle;
|
fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle;
|
||||||
|
|
||||||
/// Calculates the layout of this variant.
|
/// 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.
|
/// A conjoined group of items that function together as a button.
|
||||||
#[derive(Setters)]
|
#[derive(Setters)]
|
||||||
pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
pub struct SegmentedButton<'a, Variant, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +73,7 @@ where
|
||||||
/// iced widget ID
|
/// iced widget ID
|
||||||
pub(super) id: Option<Id>,
|
pub(super) id: Option<Id>,
|
||||||
/// The icon used for the close button.
|
/// 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.
|
/// Show the close icon only when item is hovered.
|
||||||
pub(super) show_close_icon_on_hover: bool,
|
pub(super) show_close_icon_on_hover: bool,
|
||||||
/// Padding around a button.
|
/// Padding around a button.
|
||||||
|
|
@ -92,15 +83,13 @@ where
|
||||||
/// Spacing between icon and text in button.
|
/// Spacing between icon and text in button.
|
||||||
pub(super) button_spacing: u16,
|
pub(super) button_spacing: u16,
|
||||||
/// Desired font for active tabs.
|
/// 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.
|
/// 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.
|
/// Desired font for inactive tabs.
|
||||||
pub(super) font_inactive: Option<Renderer::Font>,
|
pub(super) font_inactive: Option<crate::font::Font>,
|
||||||
/// Size of the font.
|
/// Size of the font.
|
||||||
pub(super) font_size: f32,
|
pub(super) font_size: f32,
|
||||||
/// Size of icon
|
|
||||||
pub(super) icon_size: u16,
|
|
||||||
/// Desired width of the widget.
|
/// Desired width of the widget.
|
||||||
pub(super) width: Length,
|
pub(super) width: Length,
|
||||||
/// Desired height of the widget.
|
/// Desired height of the widget.
|
||||||
|
|
@ -111,7 +100,7 @@ where
|
||||||
pub(super) line_height: LineHeight,
|
pub(super) line_height: LineHeight,
|
||||||
/// Style to draw the widget in.
|
/// Style to draw the widget in.
|
||||||
#[setters(into)]
|
#[setters(into)]
|
||||||
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
|
pub(super) style: Style,
|
||||||
/// Emits the ID of the item that was activated.
|
/// Emits the ID of the item that was activated.
|
||||||
#[setters(strip_option)]
|
#[setters(strip_option)]
|
||||||
pub(super) on_activate: Option<fn(Entity) -> Message>,
|
pub(super) on_activate: Option<fn(Entity) -> Message>,
|
||||||
|
|
@ -122,15 +111,9 @@ where
|
||||||
variant: PhantomData<Variant>,
|
variant: PhantomData<Variant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Variant, SelectionMode, Message, Renderer>
|
impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message>
|
||||||
SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
Self: SegmentedVariant,
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Self: SegmentedVariant<Renderer = Renderer>,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
{
|
{
|
||||||
|
|
@ -139,7 +122,9 @@ where
|
||||||
Self {
|
Self {
|
||||||
model,
|
model,
|
||||||
id: None,
|
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,
|
show_close_icon_on_hover: false,
|
||||||
button_padding: [4, 4, 4, 4],
|
button_padding: [4, 4, 4, 4],
|
||||||
button_height: 32,
|
button_height: 32,
|
||||||
|
|
@ -148,12 +133,11 @@ where
|
||||||
font_hovered: None,
|
font_hovered: None,
|
||||||
font_inactive: None,
|
font_inactive: None,
|
||||||
font_size: 14.0,
|
font_size: 14.0,
|
||||||
icon_size: 16,
|
|
||||||
height: Length::Shrink,
|
height: Length::Shrink,
|
||||||
width: Length::Fill,
|
width: Length::Fill,
|
||||||
spacing: 0,
|
spacing: 0,
|
||||||
line_height: LineHeight::default(),
|
line_height: LineHeight::default(),
|
||||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
style: Style::default(),
|
||||||
on_activate: None,
|
on_activate: None,
|
||||||
on_close: None,
|
on_close: None,
|
||||||
variant: PhantomData,
|
variant: PhantomData,
|
||||||
|
|
@ -238,15 +222,16 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add icon to measurement if icon was given.
|
// Add icon to measurement if icon was given.
|
||||||
if self.model.icon(key).is_some() {
|
if let Some(icon) = self.model.icon(key) {
|
||||||
button_height = button_height.max(f32::from(self.icon_size));
|
button_height = button_height.max(f32::from(icon.size));
|
||||||
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing);
|
button_width += f32::from(icon.size) + f32::from(self.button_spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add close button to measurement if found.
|
// Add close button to measurement if found.
|
||||||
if self.model.is_closable(key) {
|
if self.model.is_closable(key) {
|
||||||
button_height = button_height.max(f32::from(self.icon_size));
|
button_height = button_height.max(f32::from(self.close_icon.size));
|
||||||
button_width += f32::from(self.icon_size) + f32::from(self.button_spacing) + 8.0;
|
button_width +=
|
||||||
|
f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
height = height.max(button_height);
|
height = height.max(button_height);
|
||||||
|
|
@ -262,15 +247,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Variant, SelectionMode, Message, Renderer> Widget<Message, Renderer>
|
impl<'a, Variant, SelectionMode, Message> Widget<Message, Renderer>
|
||||||
for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>
|
for SegmentedButton<'a, Variant, SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
Self: SegmentedVariant,
|
||||||
+ iced_core::text::Renderer
|
|
||||||
+ iced_core::image::Renderer
|
|
||||||
+ iced_core::svg::Renderer,
|
|
||||||
Renderer::Theme: StyleSheet,
|
|
||||||
Self: SegmentedVariant<Renderer = Renderer>,
|
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
Message: 'static + Clone,
|
Message: 'static + Clone,
|
||||||
|
|
@ -325,7 +305,7 @@ where
|
||||||
if let Some(on_close) = self.on_close.as_ref() {
|
if let Some(on_close) = self.on_close.as_ref() {
|
||||||
if cursor_position.is_over(close_bounds(
|
if cursor_position.is_over(close_bounds(
|
||||||
bounds,
|
bounds,
|
||||||
f32::from(self.icon_size),
|
f32::from(self.close_icon.size),
|
||||||
self.button_padding,
|
self.button_padding,
|
||||||
)) {
|
)) {
|
||||||
if let Event::Mouse(mouse::Event::ButtonReleased(
|
if let Event::Mouse(mouse::Event::ButtonReleased(
|
||||||
|
|
@ -429,11 +409,11 @@ where
|
||||||
&self,
|
&self,
|
||||||
tree: &Tree,
|
tree: &Tree,
|
||||||
renderer: &mut Renderer,
|
renderer: &mut Renderer,
|
||||||
theme: &<Renderer as iced_core::Renderer>::Theme,
|
theme: &crate::Theme,
|
||||||
_style: &renderer::Style,
|
style: &renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
_cursor_position: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
_viewport: &iced::Rectangle,
|
viewport: &iced::Rectangle,
|
||||||
) {
|
) {
|
||||||
let state = tree.state.downcast_ref::<LocalState>();
|
let state = tree.state.downcast_ref::<LocalState>();
|
||||||
let appearance = Self::variant_appearance(theme, &self.style);
|
let appearance = Self::variant_appearance(theme, &self.style);
|
||||||
|
|
@ -479,12 +459,6 @@ where
|
||||||
status_appearance.middle
|
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.
|
// Render the background of the button.
|
||||||
if status_appearance.background.is_some() {
|
if status_appearance.background.is_some() {
|
||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
|
|
@ -530,27 +504,29 @@ where
|
||||||
bounds.height -=
|
bounds.height -=
|
||||||
f32::from(self.button_padding[1]) - f32::from(self.button_padding[3]);
|
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);
|
let offset = width + f32::from(self.button_spacing);
|
||||||
bounds.y = y - width / 2.0;
|
bounds.y = y - width / 2.0;
|
||||||
|
|
||||||
let icon_bounds = Rectangle {
|
let mut layout_node = layout::Node::new(Size {
|
||||||
width,
|
width,
|
||||||
height: width,
|
height: width - offset,
|
||||||
..bounds
|
});
|
||||||
};
|
layout_node.move_to(Point {
|
||||||
|
x: bounds.x + offset,
|
||||||
|
y: bounds.y,
|
||||||
|
});
|
||||||
|
|
||||||
bounds.x += offset;
|
Widget::<Message, Renderer>::draw(
|
||||||
bounds.width -= offset;
|
&Element::<Message>::from(icon.clone()),
|
||||||
|
&Tree::empty(),
|
||||||
match icon.load(self.icon_size, None, false, true) {
|
renderer,
|
||||||
icon::Handle::Image(_handle) => {
|
theme,
|
||||||
unimplemented!()
|
style,
|
||||||
}
|
Layout::new(&layout_node),
|
||||||
icon::Handle::Svg(handle) => {
|
cursor,
|
||||||
iced_core::svg::Renderer::draw(renderer, handle, icon_color, icon_bounds);
|
viewport,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
alignment::Horizontal::Left
|
alignment::Horizontal::Left
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -581,22 +557,27 @@ where
|
||||||
|
|
||||||
// Draw a close button if this is set.
|
// Draw a close button if this is set.
|
||||||
if show_close_button {
|
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 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) {
|
Widget::<Message, Renderer>::draw(
|
||||||
icon::Handle::Image(_handle) => {
|
&Element::<Message>::from(self.close_icon.clone()),
|
||||||
unimplemented!()
|
&Tree::empty(),
|
||||||
}
|
renderer,
|
||||||
icon::Handle::Svg(handle) => {
|
theme,
|
||||||
iced_core::svg::Renderer::draw(
|
style,
|
||||||
renderer,
|
Layout::new(&layout_node),
|
||||||
handle,
|
cursor,
|
||||||
Some(status_appearance.text_color),
|
viewport,
|
||||||
icon_bounds,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -611,24 +592,16 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Variant, SelectionMode, Message, Renderer>
|
impl<'a, Variant, SelectionMode, Message> From<SegmentedButton<'a, Variant, SelectionMode, Message>>
|
||||||
From<SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>>
|
for Element<'a, Message>
|
||||||
for Element<'a, Message, Renderer>
|
|
||||||
where
|
where
|
||||||
Renderer: iced_core::Renderer
|
SegmentedButton<'a, Variant, SelectionMode, Message>: SegmentedVariant,
|
||||||
+ 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>,
|
|
||||||
Variant: 'static,
|
Variant: 'static,
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
Message: 'static + Clone,
|
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() {
|
if widget.model.items.is_empty() {
|
||||||
widget.spacing = 0;
|
widget.spacing = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use super::segmented_button::{
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn horizontal<SelectionMode: Default, Message>(
|
pub fn horizontal<SelectionMode: Default, Message>(
|
||||||
model: &Model<SelectionMode>,
|
model: &Model<SelectionMode>,
|
||||||
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
|
) -> HorizontalSegmentedButton<SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
{
|
{
|
||||||
|
|
@ -36,7 +36,7 @@ where
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn vertical<SelectionMode, Message>(
|
pub fn vertical<SelectionMode, Message>(
|
||||||
model: &Model<SelectionMode>,
|
model: &Model<SelectionMode>,
|
||||||
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
|
) -> VerticalSegmentedButton<SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,18 @@
|
||||||
|
|
||||||
use std::borrow::Cow;
|
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 derive_setters::Setters;
|
||||||
use iced::widget::{column, horizontal_space, row, Row};
|
|
||||||
|
|
||||||
/// A settings item aligned in a row
|
/// A settings item aligned in a row
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub fn item<'a, Message: 'static>(
|
pub fn item<'a, Message: 'static>(
|
||||||
title: impl Into<Cow<'a, str>>,
|
title: impl Into<Cow<'a, str>> + 'a,
|
||||||
widget: impl Into<Element<'a, Message>>,
|
widget: impl Into<Element<'a, Message>> + 'a,
|
||||||
) -> Row<'a, Message, Renderer> {
|
) -> Row<'a, Message, Renderer> {
|
||||||
item_row(vec![
|
item_row(vec![
|
||||||
text(title).into(),
|
text(title).into(),
|
||||||
|
|
@ -25,7 +27,7 @@ pub fn item<'a, Message: 'static>(
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Renderer> {
|
pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Renderer> {
|
||||||
row(children)
|
row::with_children(children)
|
||||||
.align_items(iced::Alignment::Center)
|
.align_items(iced::Alignment::Center)
|
||||||
.padding([0, 18])
|
.padding([0, 18])
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
|
|
@ -65,10 +67,12 @@ impl<'a, Message: 'static> Item<'a, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(description) = self.description {
|
if let Some(description) = self.description {
|
||||||
let title = text(self.title);
|
let column = column::with_capacity(2)
|
||||||
let desc = text(description).size(10);
|
.spacing(2)
|
||||||
|
.push(text(self.title))
|
||||||
|
.push(text(description).size(10));
|
||||||
|
|
||||||
contents.push(column!(title, desc).spacing(2).into());
|
contents.push(column.into());
|
||||||
} else {
|
} else {
|
||||||
contents.push(text(self.title).into());
|
contents.push(text(self.title).into());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@ mod section;
|
||||||
pub use self::item::{item, item_row};
|
pub use self::item::{item, item_row};
|
||||||
pub use self::section::{view_section, Section};
|
pub use self::section::{view_section, Section};
|
||||||
|
|
||||||
|
use crate::widget::{column, Column};
|
||||||
use crate::{Element, Renderer};
|
use crate::{Element, Renderer};
|
||||||
use iced::widget::{column, Column};
|
|
||||||
|
|
||||||
/// A column with a predefined style for creating a settings panel
|
/// A column with a predefined style for creating a settings panel
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn view_column<Message: 'static>(children: Vec<Element<Message>>) -> Column<Message, Renderer> {
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::widget::ListColumn;
|
use crate::widget::{column, text, ListColumn};
|
||||||
use crate::Element;
|
use crate::Element;
|
||||||
use iced::widget::{column, text};
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// A section within a settings view column.
|
/// 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> {
|
impl<'a, Message: 'static> From<Section<'a, Message>> for Element<'a, Message> {
|
||||||
fn from(data: Section<'a, Message>) -> Self {
|
fn from(data: Section<'a, Message>) -> Self {
|
||||||
let title = text(data.title).font(crate::font::FONT_SEMIBOLD).into();
|
column::with_capacity(2)
|
||||||
|
|
||||||
column(vec![title, data.children.into_element()])
|
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
|
.push(text(data.title).font(crate::font::FONT_SEMIBOLD))
|
||||||
|
.push(data.children)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
pub use self::model::{Message, Model};
|
pub use self::model::{Message, Model};
|
||||||
|
|
||||||
use crate::widget::{icon, text};
|
use crate::widget::{button, container, icon, row, text};
|
||||||
use crate::{theme, Element};
|
use crate::{theme, Element};
|
||||||
use apply::Apply;
|
use apply::Apply;
|
||||||
use iced::{
|
use iced::{
|
||||||
alignment::{Horizontal, Vertical},
|
alignment::{Horizontal, Vertical},
|
||||||
widget::{button, container, row},
|
|
||||||
Alignment, Length,
|
Alignment, Length,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -42,9 +41,10 @@ impl<'a, Message: 'static> SpinButton<'a, Message> {
|
||||||
pub fn into_element(self) -> Element<'a, Message> {
|
pub fn into_element(self) -> Element<'a, Message> {
|
||||||
let Self { on_change, label } = self;
|
let Self { on_change, label } = self;
|
||||||
container(
|
container(
|
||||||
row![
|
row::with_children(vec![
|
||||||
icon("list-remove-symbolic", 24)
|
icon::handle::from_name("list-remove-symbolic")
|
||||||
.style(theme::Svg::Symbolic)
|
.size(24)
|
||||||
|
.icon()
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.width(Length::Fixed(32.0))
|
.width(Length::Fixed(32.0))
|
||||||
.height(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))
|
.width(Length::Fixed(32.0))
|
||||||
.height(Length::Fixed(32.0))
|
.height(Length::Fixed(32.0))
|
||||||
.style(theme::Button::Text)
|
.style(theme::Button::Text)
|
||||||
.on_press(model::Message::Decrement),
|
.on_press(model::Message::Decrement)
|
||||||
|
.into(),
|
||||||
text(label)
|
text(label)
|
||||||
.vertical_alignment(Vertical::Center)
|
.vertical_alignment(Vertical::Center)
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.align_x(Horizontal::Center)
|
.align_x(Horizontal::Center)
|
||||||
.align_y(Vertical::Center),
|
.align_y(Vertical::Center)
|
||||||
icon("list-add-symbolic", 24)
|
.into(),
|
||||||
.style(theme::Svg::Symbolic)
|
icon::handle::from_name("list-add-symbolic")
|
||||||
|
.size(24)
|
||||||
|
.icon()
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.width(Length::Fixed(32.0))
|
.width(Length::Fixed(32.0))
|
||||||
.height(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))
|
.width(Length::Fixed(32.0))
|
||||||
.height(Length::Fixed(32.0))
|
.height(Length::Fixed(32.0))
|
||||||
.style(theme::Button::Text)
|
.style(theme::Button::Text)
|
||||||
.on_press(model::Message::Increment),
|
.on_press(model::Message::Increment)
|
||||||
]
|
.into(),
|
||||||
|
])
|
||||||
.width(Length::Shrink)
|
.width(Length::Shrink)
|
||||||
.height(Length::Fixed(32.0))
|
.height(Length::Fixed(32.0))
|
||||||
.spacing(4.0)
|
.spacing(4.0)
|
||||||
|
|
@ -101,6 +105,7 @@ fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance {
|
||||||
let accent = &basic.accent;
|
let accent = &basic.accent;
|
||||||
let corners = &basic.corner_radii;
|
let corners = &basic.corner_radii;
|
||||||
iced_style::container::Appearance {
|
iced_style::container::Appearance {
|
||||||
|
icon_color: Some(basic.palette.neutral_10.into()),
|
||||||
text_color: Some(basic.palette.neutral_10.into()),
|
text_color: Some(basic.palette.neutral_10.into()),
|
||||||
background: None,
|
background: None,
|
||||||
border_radius: corners.radius_s.into(),
|
border_radius: corners.radius_s.into(),
|
||||||
|
|
|
||||||
|
|
@ -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.
|
//! Track the cursor of a text input.
|
||||||
use super::value::Value;
|
use super::value::Value;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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};
|
use super::{cursor::Cursor, value::Value};
|
||||||
|
|
||||||
pub struct Editor<'a> {
|
pub struct Editor<'a> {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
//! Display fields that can be filled with text.
|
||||||
//!
|
//!
|
||||||
//! A [`TextInput`] has some local [`State`].
|
//! A [`TextInput`] has some local [`State`].
|
||||||
|
|
@ -9,6 +13,7 @@ use super::editor::Editor;
|
||||||
use super::style::StyleSheet;
|
use super::style::StyleSheet;
|
||||||
pub use super::value::Value;
|
pub use super::value::Value;
|
||||||
|
|
||||||
|
use apply::Apply;
|
||||||
use iced::Limits;
|
use iced::Limits;
|
||||||
use iced_core::event::{self, Event};
|
use iced_core::event::{self, Event};
|
||||||
use iced_core::keyboard;
|
use iced_core::keyboard;
|
||||||
|
|
@ -66,8 +71,9 @@ where
|
||||||
.style(super::style::TextInput::Search)
|
.style(super::style::TextInput::Search)
|
||||||
.start_icon(
|
.start_icon(
|
||||||
iced_widget::container(
|
iced_widget::container(
|
||||||
crate::widget::icon("system-search-symbolic", 16)
|
crate::widget::icon::handle::from_name("system-search-symbolic")
|
||||||
.style(crate::theme::Svg::Symbolic),
|
.size(16)
|
||||||
|
.icon(),
|
||||||
)
|
)
|
||||||
.padding([spacing, spacing, spacing, spacing])
|
.padding([spacing, spacing, spacing, spacing])
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -75,8 +81,10 @@ where
|
||||||
|
|
||||||
if let Some(msg) = on_clear {
|
if let Some(msg) = on_clear {
|
||||||
input.end_icon(
|
input.end_icon(
|
||||||
crate::widget::button::button(crate::theme::Button::Text)
|
crate::widget::icon::handle::from_name("edit-clear-symbolic")
|
||||||
.icon(crate::theme::Svg::Symbolic, "edit-clear-symbolic", 16)
|
.size(16)
|
||||||
|
.handle()
|
||||||
|
.apply(crate::widget::button::icon)
|
||||||
.on_press(msg)
|
.on_press(msg)
|
||||||
.padding([spacing, spacing, spacing, spacing])
|
.padding([spacing, spacing, spacing, spacing])
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -102,24 +110,22 @@ where
|
||||||
.padding([0, spacing, 0, spacing])
|
.padding([0, spacing, 0, spacing])
|
||||||
.style(super::style::TextInput::Default)
|
.style(super::style::TextInput::Default)
|
||||||
.start_icon(
|
.start_icon(
|
||||||
iced_widget::container(
|
crate::widget::icon::handle::from_name("system-lock-screen-symbolic")
|
||||||
crate::widget::icon("system-lock-screen-symbolic", 16)
|
.size(16)
|
||||||
.style(crate::theme::Svg::Symbolic),
|
.icon()
|
||||||
)
|
.apply(iced_widget::container)
|
||||||
.padding([spacing, spacing, spacing, spacing])
|
.padding([spacing, spacing, spacing, spacing])
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
if hidden {
|
if hidden {
|
||||||
input = input.password();
|
input = input.password();
|
||||||
}
|
}
|
||||||
if let Some(msg) = on_visible_toggle {
|
if let Some(msg) = on_visible_toggle {
|
||||||
input.end_icon(
|
input.end_icon(
|
||||||
crate::widget::button::button(crate::theme::Button::Text)
|
crate::widget::icon::handle::from_name("document-properties-symbolic")
|
||||||
.icon(
|
.size(16)
|
||||||
crate::theme::Svg::Symbolic,
|
.handle()
|
||||||
"document-properties-symbolic",
|
.apply(crate::widget::button::icon)
|
||||||
16,
|
|
||||||
)
|
|
||||||
.on_press(msg)
|
.on_press(msg)
|
||||||
.padding([spacing, spacing, spacing, spacing])
|
.padding([spacing, spacing, spacing, spacing])
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -300,8 +306,14 @@ where
|
||||||
|
|
||||||
/// Sets the message that should be produced when the [`TextInput`] is
|
/// Sets the message that should be produced when the [`TextInput`] is
|
||||||
/// focused and the enter key is pressed.
|
/// focused and the enter key is pressed.
|
||||||
pub fn on_submit(mut self, message: Message) -> Self {
|
pub fn on_submit(self, message: Message) -> Self {
|
||||||
self.on_submit = Some(message);
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1846,6 +1858,7 @@ pub fn draw<'a, Message>(
|
||||||
renderer,
|
renderer,
|
||||||
theme,
|
theme,
|
||||||
&renderer::Style {
|
&renderer::Style {
|
||||||
|
icon_color: renderer_style.icon_color,
|
||||||
text_color: appearance.text_color,
|
text_color: appearance.text_color,
|
||||||
scale_factor: renderer_style.scale_factor,
|
scale_factor: renderer_style.scale_factor,
|
||||||
},
|
},
|
||||||
|
|
@ -2013,6 +2026,7 @@ pub fn draw<'a, Message>(
|
||||||
renderer,
|
renderer,
|
||||||
theme,
|
theme,
|
||||||
&renderer::Style {
|
&renderer::Style {
|
||||||
|
icon_color: renderer_style.icon_color,
|
||||||
text_color: appearance.text_color,
|
text_color: appearance.text_color,
|
||||||
scale_factor: renderer_style.scale_factor,
|
scale_factor: renderer_style.scale_factor,
|
||||||
},
|
},
|
||||||
|
|
@ -2063,11 +2077,9 @@ pub struct TextInputString(String);
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
impl DataFromMimeType for TextInputString {
|
impl DataFromMimeType for TextInputString {
|
||||||
fn from_mime_type(&self, mime_type: &str) -> Option<Vec<u8>> {
|
fn from_mime_type(&self, mime_type: &str) -> Option<Vec<u8>> {
|
||||||
if SUPPORTED_MIME_TYPES.contains(&mime_type) {
|
SUPPORTED_MIME_TYPES
|
||||||
Some(self.0.as_bytes().to_vec())
|
.contains(&mime_type)
|
||||||
} else {
|
.then(|| self.0.as_bytes().to_vec())
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
//! A text input widget from iced widgets plus some added details.
|
||||||
|
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
//! Change the appearance of a text input.
|
||||||
use iced_core::{Background, BorderRadius, Color};
|
use iced_core::{Background, BorderRadius, Color};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
/// The value of a [`TextInput`].
|
/// The value of a [`TextInput`].
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use super::segmented_button::{
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn horizontal<SelectionMode: Default, Message>(
|
pub fn horizontal<SelectionMode: Default, Message>(
|
||||||
model: &Model<SelectionMode>,
|
model: &Model<SelectionMode>,
|
||||||
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
|
) -> HorizontalSegmentedButton<SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
{
|
{
|
||||||
|
|
@ -36,7 +36,7 @@ where
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn vertical<SelectionMode, Message>(
|
pub fn vertical<SelectionMode, Message>(
|
||||||
model: &Model<SelectionMode>,
|
model: &Model<SelectionMode>,
|
||||||
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
|
) -> VerticalSegmentedButton<SelectionMode, Message>
|
||||||
where
|
where
|
||||||
Model<SelectionMode>: Selectable,
|
Model<SelectionMode>: Selectable,
|
||||||
SelectionMode: Default,
|
SelectionMode: Default,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
// Copyright 2022 System76 <info@system76.com>
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use iced::{alignment, widget, Alignment, Background, Color, Length};
|
|
||||||
|
|
||||||
use crate::{theme, Element, Renderer, Theme};
|
|
||||||
|
|
||||||
use super::icon;
|
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]
|
#[must_use]
|
||||||
pub fn warning<'a, Message>(message: impl Into<Cow<'a, str>>) -> Warning<'a, Message> {
|
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.
|
/// A custom button that has the desired default spacing and padding.
|
||||||
pub fn into_widget(self) -> widget::Container<'a, Message, Renderer> {
|
pub fn into_widget(self) -> widget::Container<'a, Message, Renderer> {
|
||||||
let close_button =
|
let label = widget::container(crate::widget::text(self.message)).width(Length::Fill);
|
||||||
widget::button(icon("window-close-symbolic", 16).style(theme::Svg::Default))
|
|
||||||
.style(theme::Button::Transparent);
|
|
||||||
|
|
||||||
let close_button = if let Some(message) = self.on_close {
|
let close_button = icon::handle::from_name("window-close-symbolic")
|
||||||
close_button.on_press(message)
|
.size(16)
|
||||||
} else {
|
.apply(widget::button::icon)
|
||||||
close_button
|
.on_press_maybe(self.on_close);
|
||||||
};
|
|
||||||
|
|
||||||
widget::container(
|
widget::row::with_capacity(2)
|
||||||
widget::row(vec![
|
.push(label)
|
||||||
widget::container(crate::widget::text(self.message))
|
.push(close_button)
|
||||||
.width(Length::Fill)
|
.align_items(Alignment::Center)
|
||||||
.into(),
|
.apply(widget::container)
|
||||||
close_button.into(),
|
.style(theme::Container::custom(warning_container))
|
||||||
])
|
.padding(10)
|
||||||
.align_items(Alignment::Center),
|
.align_y(alignment::Vertical::Center)
|
||||||
)
|
.width(Length::Fill)
|
||||||
.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]
|
#[must_use]
|
||||||
pub fn warning_container(theme: &Theme) -> widget::container::Appearance {
|
pub fn warning_container(theme: &Theme) -> widget::container::Appearance {
|
||||||
widget::container::Appearance {
|
widget::container::Appearance {
|
||||||
|
icon_color: Some(theme.cosmic().warning.on.into()),
|
||||||
text_color: Some(theme.cosmic().warning.on.into()),
|
text_color: Some(theme.cosmic().warning.on.into()),
|
||||||
background: Some(Background::Color(theme.cosmic().warning_color().into())),
|
background: Some(Background::Color(theme.cosmic().warning_color().into())),
|
||||||
border_radius: 0.0.into(),
|
border_radius: 0.0.into(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue