Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
Vukašin Vojinović
95756b1a57 improv(circular): prevent caps from touching 2026-04-18 16:08:34 -04:00
Vukašin Vojinović
c423ad1bfc improv(about): use ListButton 2026-04-17 13:52:08 +02:00
Vukašin Vojinović
8d7bcab258 fix(list_column): add back divider_padding
Also matches previous behavior of both paddings being applied to subsequent items, rather than globally.
2026-04-17 13:17:23 +02:00
Hojjat
c162a1f24a fix(animated-image): update frames and fix compilation errors 2026-04-16 19:58:39 +02:00
Vukašin Vojinović
3f9e93067b fix(item builder): remove unnecessary lifetime bound for radio 2026-04-16 18:26:17 +02:00
Vukašin Vojinović
917af9fda2 feat(radio): internal method for radio without label
Also adds the related settings item builder.
2026-04-16 17:19:36 +02:00
Vukašin Vojinović
9b465a8b5c feat(list_column): button list items 2026-04-16 17:19:36 +02:00
Vukašin Vojinović
9cac422c24 fix(toggler): animate external changes 2026-04-16 17:19:36 +02:00
Hojjat
0fc4638af3 fix: register image_extras in run_single_instance too 2026-04-15 23:59:57 +02:00
Hojjat
3d8d8915be chore: enable ico and xpm image support for desktop feature 2026-04-15 13:10:25 +02:00
Ian Douglas Scott
46d9f0c344 widget/icon: Bundle icons on macOS, not just Windows 2026-04-14 21:46:05 +02:00
Jeremy Soller
0d69cd9183
i18n: translation update from Hosted Weblate (#1177)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop
OS/libcosmic](https://hosted.weblate.org/projects/pop-os/libcosmic/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/libcosmic/horizontal-auto.svg)
2026-04-14 09:52:02 -06:00
Hojjat
52116d2f36 chore: update iced 2026-04-13 22:26:33 +02:00
Hosted Weblate
0e72508dcc
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Asier Saratsua Garmendia <asier.sarasua@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hant/
Translation: Pop OS/libcosmic
2026-04-12 18:50:19 +02:00
Jeremy Soller
1d7113a244
chore: update iced (#1240)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-04-11 06:27:56 -06:00
Hojjat
e287a789c1 chore: update iced 2026-04-10 20:53:43 -06:00
Hojjat
6caccaba33 fix: icon color when window is maximized 2026-04-09 12:54:32 -04:00
26 changed files with 643 additions and 325 deletions

View file

@ -58,6 +58,7 @@ desktop = [
"process", "process",
"dep:cosmic-settings-config", "dep:cosmic-settings-config",
"dep:freedesktop-desktop-entry", "dep:freedesktop-desktop-entry",
"dep:image-extras",
"dep:mime", "dep:mime",
"dep:shlex", "dep:shlex",
"tokio?/io-util", "tokio?/io-util",
@ -141,9 +142,14 @@ css-color = "0.2.8"
derive_setters = "0.1.9" derive_setters = "0.1.9"
futures = "0.3" futures = "0.3"
image = { version = "0.25.10", default-features = false, features = [ image = { version = "0.25.10", default-features = false, features = [
"ico",
"jpeg", "jpeg",
"png", "png",
] } ] }
image-extras = { version = "0.1.0", default-features = false, features = [
"xpm",
"xbm",
], optional = true }
libc = { version = "0.2.183", optional = true } libc = { version = "0.2.183", optional = true }
log = "0.4" log = "0.4"
mime = { version = "0.3.17", optional = true } mime = { version = "0.3.17", optional = true }
@ -170,12 +176,12 @@ cosmic-config = { path = "cosmic-config", features = ["dbus"] }
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" }
zbus = { version = "5.14.0", default-features = false } zbus = { version = "5.14.0", default-features = false }
[target.'cfg(unix)'.dependencies] [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" }
freedesktop-desktop-entry = { version = "0.8.1", optional = true } freedesktop-desktop-entry = { version = "0.8.1", optional = true }
shlex = { version = "1.3.0", optional = true } shlex = { version = "1.3.0", optional = true }
[target.'cfg(not(unix))'.dependencies] [target.'cfg(any(not(unix), target_os = "macos"))'.dependencies]
# Used to embed bundled icons for non-unix platforms. # Used to embed bundled icons for non-unix platforms.
phf = { version = "0.13.1", features = ["macros"] } phf = { version = "0.13.1", features = ["macros"] }

View file

@ -3,7 +3,9 @@ use std::env;
fn main() { fn main() {
println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=build.rs");
if env::var_os("CARGO_CFG_UNIX").is_none() { if env::var_os("CARGO_CFG_UNIX").is_none()
|| env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos")
{
generate_bundled_icons(); generate_bundled_icons();
} }
} }

View file

@ -21,4 +21,5 @@ features = [
"single-instance", "single-instance",
"surface-message", "surface-message",
"multi-window", "multi-window",
"wgpu",
] ]

View file

@ -200,7 +200,7 @@ impl cosmic::Application for App {
.map_or("No page selected", String::as_str); .map_or("No page selected", String::as_str);
let centered = widget::container( let centered = widget::container(
widget::column::with_capacity(5) widget::column::with_capacity(14)
.push(widget::text::body(page_content)) .push(widget::text::body(page_content))
.push( .push(
widget::text_input::text_input("", &self.input_1) widget::text_input::text_input("", &self.input_1)
@ -223,6 +223,7 @@ impl cosmic::Application for App {
.on_clear(Message::Ignore), .on_clear(Message::Ignore),
) )
.push(widget::progress_bar::circular::Circular::new().size(50.0)) .push(widget::progress_bar::circular::Circular::new().size(50.0))
.push(widget::progress_bar::circular::Circular::new().size(20.0))
.push( .push(
widget::progress_bar::linear::Linear::new() widget::progress_bar::linear::Linear::new()
.girth(10.0) .girth(10.0)

View file

@ -6,7 +6,7 @@ links = Links
developers = Entwickler(innen) developers = Entwickler(innen)
designers = Designer(innen) designers = Designer(innen)
artists = Künstler(innen) artists = Künstler(innen)
translators = Übersetzer*innen translators = Übersetzer(innen)
documenters = Dokumentierer(innen) documenters = Dokumentierer(innen)
# Calendar # Calendar
january = Januar { $year } january = Januar { $year }

0
i18n/eu/libcosmic.ftl Normal file
View file

View file

@ -0,0 +1,33 @@
close = Mdel
license = Turagt
links = Iseɣwan
developers = Ineflayen
artists = Inaẓuren
translators = Imsuqlen
january = Yennayer { $year }
february = Fuṛar { $year }
march = Meɣres { $year }
april = Yebrir { $year }
may = Mayyu { $year }
june = Yunyu { $year }
july = Yulyu { $year }
august = Ɣuct { $year }
september = Ctembeṛ { $year }
october = Tubeṛ { $year }
november = Wambeṛ { $year }
december = Dujembeṛ { $year }
documenters = Imeskaren
monday = Arim
mon = Ari
tuesday = Aram
tue = Ara
wednesday = Ahad
wed = Aha
thursday = Amhad
thu = Amh
friday = Sem
fri = Sm
saturday = Sed
sat = Sd
sunday = Acer
sun = Ace

View file

@ -2,26 +2,33 @@ february = { $year }년 2월
close = 닫기 close = 닫기
documenters = 문서 작성자 documenters = 문서 작성자
november = { $year }년 11월 november = { $year }년 11월
friday = 금 friday = 금요일
tuesday = 화 tuesday = 화요일
may = { $year }년 5월 may = { $year }년 5월
wednesday = 수 wednesday = 수요일
april = { $year }년 4월 april = { $year }년 4월
monday = 월 monday = 월요일
translators = 번역가 translators = 번역가
artists = 아티스트 artists = 아티스트
license = 라이선스 license = 라이선스
december = { $year }년 12월 december = { $year }년 12월
sunday = 일 sunday = 일요일
links = 링크 links = 링크
march = { $year }년 3월 march = { $year }년 3월
june = { $year }년 6월 june = { $year }년 6월
saturday = 토 saturday = 토요일
august = { $year }년 8월 august = { $year }년 8월
developers = 개발자 developers = 개발자
july = { $year }년 7월 july = { $year }년 7월
thursday = 목 thursday = 목요일
september = { $year }년 9월 september = { $year }년 9월
designers = 디자이너 designers = 디자이너
october = { $year }년 10월 october = { $year }년 10월
january = { $year }년 1월 january = { $year }년 1월
mon = 월
tue = 화
wed = 수
thu = 목
fri = 금
sat = 토
sun = 일

View file

@ -0,0 +1,34 @@
close = 關閉
developers = 開發人員
designers = 設計人員
artists = 美編設計
translators = 翻譯人員
documenters = 文件編輯人員
january = { $year } 年 1 月
monday = 星期一
tuesday = 星期二
wednesday = 星期三
thursday = 星期四
friday = 星期五
saturday = 星期六
sunday = 星期日
mon = 週一
tue = 週二
wed = 週三
thu = 週四
fri = 週五
sat = 週六
sun = 週日
license = 授權
links = 連結
february = { $year } 年 2 月
march = { $year } 年 3 月
april = { $year } 年 4 月
may = { $year } 年 5 月
june = { $year } 年 6 月
july = { $year } 年 7 月
august = { $year } 年 8 月
september = { $year } 年 9 月
october = { $year } 年 10 月
november = { $year } 年 11 月
december = { $year } 年 12 月

2
iced

@ -1 +1 @@
Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece

View file

@ -128,6 +128,9 @@ impl<A: crate::app::Application> BootFn<cosmic::Cosmic<A>, crate::Action<A::Mess
/// ///
/// Returns error on application failure. /// Returns error on application failure.
pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result { pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result {
#[cfg(feature = "desktop")]
image_extras::register();
#[cfg(all(target_env = "gnu", not(target_os = "windows")))] #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
if let Some(threshold) = settings.default_mmap_threshold { if let Some(threshold) = settings.default_mmap_threshold {
crate::malloc::limit_mmap_threshold(threshold); crate::malloc::limit_mmap_threshold(threshold);
@ -194,6 +197,9 @@ where
App::Flags: CosmicFlags, App::Flags: CosmicFlags,
App::Message: Clone + std::fmt::Debug + Send + 'static, App::Message: Clone + std::fmt::Debug + Send + 'static,
{ {
#[cfg(feature = "desktop")]
image_extras::register();
use std::collections::HashMap; use std::collections::HashMap;
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();

View file

@ -307,7 +307,7 @@ impl DefaultStyle for Theme {
fn default_style(&self) -> Appearance { fn default_style(&self) -> Appearance {
let cosmic = self.cosmic(); let cosmic = self.cosmic();
Appearance { Appearance {
icon_color: cosmic.bg_color().into(), icon_color: cosmic.on_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(),
} }

View file

@ -27,7 +27,7 @@ pub enum Button {
IconVertical, IconVertical,
Image, Image,
Link, Link,
ListItem, ListItem([f32; 4]),
MenuFolder, MenuFolder,
MenuItem, MenuItem,
MenuRoot, MenuRoot,
@ -148,8 +148,8 @@ pub fn appearance(
appearance.text_color = Some(component.on.into()); appearance.text_color = Some(component.on.into());
corner_radii = &cosmic.corner_radii.radius_s; corner_radii = &cosmic.corner_radii.radius_s;
} }
Button::ListItem => { Button::ListItem(radii) => {
corner_radii = &[0.0; 4]; corner_radii = radii;
let (background, text, icon) = color(&cosmic.background.component); let (background, text, icon) = color(&cosmic.background.component);
if selected { if selected {
@ -197,7 +197,7 @@ impl Catalog for crate::Theme {
return active(focused, self); return active(focused, self);
} }
appearance(self, focused, selected, false, style, move |component| { let mut s = appearance(self, focused, selected, false, style, move |component| {
let text_color = if matches!( let text_color = if matches!(
style, style,
Button::Icon | Button::IconVertical | Button::HeaderBar Button::Icon | Button::IconVertical | Button::HeaderBar
@ -209,7 +209,15 @@ impl Catalog for crate::Theme {
}; };
(component.base.into(), text_color, text_color) (component.base.into(), text_color, text_color)
}) });
if let Button::ListItem(_) = style {
if !selected {
s.background = None;
}
}
s
} }
fn disabled(&self, style: &Self::Class) -> Style { fn disabled(&self, style: &Self::Class) -> Style {
@ -237,7 +245,7 @@ impl Catalog for crate::Theme {
return hovered(focused, self); return hovered(focused, self);
} }
appearance( let mut s = appearance(
self, self,
focused || matches!(style, Button::Image), focused || matches!(style, Button::Image),
selected, selected,
@ -256,7 +264,15 @@ impl Catalog for crate::Theme {
(component.hover.into(), text_color, text_color) (component.hover.into(), text_color, text_color)
}, },
) );
if let Button::ListItem(_) = style {
if !selected {
s.background = None;
}
}
s
} }
fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {

View file

@ -43,7 +43,7 @@ pub mod application {
iced::theme::Style { iced::theme::Style {
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(),
icon_color: cosmic.bg_color().into(), icon_color: cosmic.on_bg_color().into(),
} }
} }
} }

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
Apply, Element, fl, Apply, Element, fl,
iced::{Alignment, Length}, iced::{Alignment, Length},
widget::{self, space}, widget::{self, list},
}; };
use std::rc::Rc;
#[derive(Debug, Default, Clone, derive_setters::Setters)] #[derive(Debug, Default, Clone, derive_setters::Setters)]
#[setters(into, strip_option)] #[setters(into, strip_option)]
@ -104,19 +105,23 @@ pub fn about<'a, Message: Clone + 'static>(
space_xxs, space_m, .. space_xxs, space_m, ..
} = crate::theme::spacing(); } = crate::theme::spacing();
let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style {
widget::row::with_capacity(3) color: Some(theme.cosmic().accent_text_color().into()),
.push(widget::text(name)) });
.push(space::horizontal())
let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> {
widget::row::with_capacity(2)
.push(widget::text::body(name).width(Length::Fill))
.push_maybe( .push_maybe(
(!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), (!url.is_empty()).then_some(
widget::icon::from_name("link-symbolic")
.icon()
.class(crate::theme::Svg::Custom(svg_accent.clone())),
),
) )
.align_y(Alignment::Center) .align_y(Alignment::Center)
.apply(widget::button::custom) .apply(list::button)
.class(crate::theme::Button::Link)
.on_press(on_url_press(url)) .on_press(on_url_press(url))
.width(Length::Fill)
.into()
}; };
let section = |list: &'a Vec<(String, String)>, title: String| { let section = |list: &'a Vec<(String, String)>, title: String| {

View file

@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer;
use iced_core::mouse::Cursor; use iced_core::mouse::Cursor;
use iced_core::widget::{Tree, tree}; use iced_core::widget::{Tree, tree};
use iced_core::{ use iced_core::{
Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size,
event, layout, renderer, window, Widget, event, layout, renderer, window,
}; };
use iced_widget::image::{self, Handle}; use iced_widget::image::{self, FilterMethod, Handle};
use image_rs::AnimationDecoder; use image_rs::AnimationDecoder;
use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::gif::GifDecoder;
use image_rs::codecs::png::PngDecoder; use image_rs::codecs::png::PngDecoder;
@ -146,7 +146,7 @@ impl Frames {
match image_type { match image_type {
ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?),
ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?),
ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?),
} }
} }
@ -168,10 +168,10 @@ impl Frames {
let first = frames.first().cloned().unwrap(); let first = frames.first().cloned().unwrap();
let total_bytes = frames let total_bytes = frames
.iter() .iter()
.map(|f| match f.handle.data() { .map(|f| match &f.handle {
iced_core::image::Handle::Path(..) => 0, Handle::Path(..) => 0,
iced_core::image::Handle::Bytes(_, b) => b.len(), Handle::Bytes(_, b) => b.len(),
iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), Handle::Rgba { pixels, .. } => pixels.len(),
}) })
.sum::<usize>() .sum::<usize>()
.try_into() .try_into()
@ -324,7 +324,11 @@ where
&self.frames.first.handle, &self.frames.first.handle,
self.width, self.width,
self.height, self.height,
None,
self.content_fit, self.content_fit,
Rotation::default(),
false,
[0.0; 4],
) )
} }
@ -371,37 +375,18 @@ where
) { ) {
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
// Pulled from iced_native::widget::<Image as Widget>::draw iced_widget::image::draw(
// renderer,
// TODO: export iced_native::widget::image::draw as standalone function layout,
{ &state.current.frame.handle,
let Size { width, height } = renderer.dimensions(&state.current.frame.handle); None,
let image_size = Size::new(width as f32, height as f32); iced_core::border::Radius::default(),
self.content_fit,
let bounds = layout.bounds(); FilterMethod::default(),
let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); Rotation::default(),
1.0,
let render = |renderer: &mut Renderer| { 1.0,
let offset = Vector::new( );
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
);
let drawing_bounds = Rectangle {
width: adjusted_fit.width,
height: adjusted_fit.height,
..bounds
};
renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height {
renderer.with_layer(bounds, render);
} else {
render(renderer);
}
}
} }
} }

View file

@ -4,12 +4,12 @@
//! Embedded icons for platforms which do not support icon themes yet. //! Embedded icons for platforms which do not support icon themes yet.
/// Icon bundling is not enabled on unix platforms. /// Icon bundling is not enabled on unix platforms.
#[cfg(unix)] #[cfg(all(unix, not(target_os = "macos")))]
pub fn get(icon_name: &str) -> Option<super::Data> { pub fn get(icon_name: &str) -> Option<super::Data> {
None None
} }
#[cfg(not(unix))] #[cfg(any(not(unix), target_os = "macos"))]
/// Get a bundled icon on non-unix platforms. /// Get a bundled icon on non-unix platforms.
pub fn get(icon_name: &str) -> Option<super::Data> { pub fn get(icon_name: &str) -> Option<super::Data> {
ICONS ICONS
@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option<super::Data> {
.map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes)))
} }
#[cfg(not(unix))] #[cfg(any(not(unix), target_os = "macos"))]
include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs"));

View file

@ -52,7 +52,7 @@ impl Named {
} }
} }
#[cfg(not(windows))] #[cfg(all(unix, not(target_os = "macos")))]
#[must_use] #[must_use]
pub fn path(self) -> Option<PathBuf> { pub fn path(self) -> Option<PathBuf> {
let name = &*self.name; let name = &*self.name;
@ -107,7 +107,7 @@ impl Named {
result result
} }
#[cfg(windows)] #[cfg(any(not(unix), target_os = "macos"))]
#[must_use] #[must_use]
pub fn path(self) -> Option<PathBuf> { pub fn path(self) -> Option<PathBuf> {
//TODO: implement icon lookup for Windows //TODO: implement icon lookup for Windows

View file

@ -1,128 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced_core::Padding;
use iced_widget::container::Catalog;
use crate::{
Apply, Element, theme,
widget::{container, divider, space::vertical},
};
#[inline]
pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> {
ListColumn::default()
}
#[must_use]
pub struct ListColumn<'a, Message> {
spacing: u16,
padding: Padding,
list_item_padding: Padding,
divider_padding: u16,
style: theme::Container<'a>,
children: Vec<Element<'a, Message>>,
}
impl<Message: 'static> Default for ListColumn<'_, Message> {
fn default() -> Self {
let cosmic_theme::Spacing {
space_xxs, space_m, ..
} = theme::spacing();
Self {
spacing: 0,
padding: Padding::from(0),
divider_padding: 16,
list_item_padding: [space_xxs, space_m].into(),
style: theme::Container::List,
children: Vec::with_capacity(4),
}
}
}
impl<'a, Message: 'static> ListColumn<'a, Message> {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, item: impl Into<Element<'a, Message>>) -> Self {
#[inline(never)]
fn inner<'a, Message: 'static>(
mut this: ListColumn<'a, Message>,
item: Element<'a, Message>,
) -> ListColumn<'a, Message> {
if !this.children.is_empty() {
this.children.push(
container(divider::horizontal::default())
.padding([0, this.divider_padding])
.into(),
);
}
// Ensure a minimum height of 32.
let list_item = crate::widget::row![
container(item).align_y(iced::Alignment::Center),
vertical().height(iced::Length::Fixed(32.))
]
.padding(this.list_item_padding)
.align_y(iced::Alignment::Center);
this.children.push(list_item.into());
this
}
inner(self, item.into())
}
#[inline]
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
/// Sets the style variant of this [`Circular`].
#[inline]
pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
#[inline]
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
#[inline]
pub fn divider_padding(mut self, padding: u16) -> Self {
self.divider_padding = padding;
self
}
pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> Self {
self.list_item_padding = padding.into();
self
}
#[must_use]
pub fn into_element(self) -> Element<'a, Message> {
crate::widget::column::with_children(self.children)
.spacing(self.spacing)
.padding(self.padding)
.width(iced::Length::Fill)
.apply(container)
.padding([self.spacing, 0])
.class(self.style)
.width(iced::Length::Fill)
.into()
}
}
impl<'a, Message: 'static> From<ListColumn<'a, Message>> for Element<'a, Message> {
fn from(column: ListColumn<'a, Message>) -> Self {
column.into_element()
}
}

View file

@ -0,0 +1,213 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::widget::container::Catalog;
use crate::widget::{button, column, container, divider, row, space::vertical};
use crate::{Apply, Element, theme};
use iced::{Length, Padding};
/// A button list item for use in a [`ListColumn`].
pub struct ListButton<'a, Message> {
content: Element<'a, Message>,
on_press: Option<Message>,
selected: bool,
}
/// Creates a [`ListButton`] with the given content.
pub fn button<'a, Message>(content: impl Into<Element<'a, Message>>) -> ListButton<'a, Message> {
ListButton {
content: content.into(),
on_press: None,
selected: false,
}
}
impl<'a, Message: 'static> ListButton<'a, Message> {
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(on_press);
self
}
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
pub enum ListItem<'a, Message> {
Element(Element<'a, Message>),
Button(ListButton<'a, Message>),
}
/// A trait for types that can be added to a [`ListColumn`].
pub trait IntoListItem<'a, Message> {
fn into_list_item(self) -> ListItem<'a, Message>;
}
impl<'a, Message, T> IntoListItem<'a, Message> for T
where
T: Into<Element<'a, Message>>,
{
fn into_list_item(self) -> ListItem<'a, Message> {
ListItem::Element(self.into())
}
}
impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> {
fn into_list_item(self) -> ListItem<'a, Message> {
ListItem::Button(self)
}
}
// Snapshots the padding values at the moment an item is added
struct ListEntry<'a, Message> {
item: ListItem<'a, Message>,
item_padding: Padding,
divider_padding: u16,
}
#[must_use]
pub struct ListColumn<'a, Message> {
list_item_padding: Padding,
divider_padding: u16,
style: theme::Container<'a>,
children: Vec<ListEntry<'a, Message>>,
}
#[inline]
pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> {
ListColumn::default()
}
pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> {
let cosmic_theme::Spacing {
space_xxs, space_m, ..
} = theme::spacing();
ListColumn {
list_item_padding: [space_xxs, space_m].into(),
divider_padding: 0,
style: theme::Container::List,
children: Vec::with_capacity(capacity),
}
}
impl<Message: 'static> Default for ListColumn<'_, Message> {
fn default() -> Self {
with_capacity(4)
}
}
impl<'a, Message: Clone + 'static> ListColumn<'a, Message> {
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Adds a [`ListItem`] to the [`ListColumn`].
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self {
self.children.push(ListEntry {
item: item.into_list_item(),
item_padding: self.list_item_padding,
divider_padding: self.divider_padding,
});
self
}
/// Sets the style variant of this [`ListColumn`].
#[inline]
pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> Self {
self.list_item_padding = padding.into();
self
}
#[inline]
pub fn divider_padding(mut self, padding: u16) -> Self {
self.divider_padding = padding;
self
}
#[must_use]
pub fn into_element(self) -> Element<'a, Message> {
let count = self.children.len();
let last_index = count.saturating_sub(1);
let radius_s = theme::active().cosmic().radius_s();
let mut col = column::with_capacity((2 * count).saturating_sub(1));
// Ensure minimum height of 32
let content_row = |content| {
row![container(content), vertical().height(32)].align_y(iced::Alignment::Center)
};
for (
i,
ListEntry {
item,
item_padding,
divider_padding,
},
) in self.children.into_iter().enumerate()
{
if i > 0 {
col = col
.push(container(divider::horizontal::default()).padding([0, divider_padding]));
}
col = match item {
ListItem::Element(content) => col.push(
content_row(content)
.padding(item_padding)
.width(Length::Fill),
),
ListItem::Button(ListButton {
content,
on_press,
selected,
}) => col.push(
content_row(content)
.apply(button::custom)
.padding(item_padding)
.width(Length::Fill)
.on_press_maybe(on_press)
.selected(selected)
.class(theme::Button::ListItem(get_radius(
radius_s,
i == 0,
i == last_index,
))),
),
};
}
col.width(Length::Fill)
.apply(container)
.class(self.style)
.into()
}
}
impl<'a, Message: Clone + 'static> From<ListColumn<'a, Message>> for Element<'a, Message> {
fn from(column: ListColumn<'a, Message>) -> Self {
column.into_element()
}
}
fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] {
match (first, last) {
(true, true) => radius,
(true, false) => [radius[0], radius[1], 0.0, 0.0],
(false, true) => [0.0, 0.0, radius[2], radius[3]],
(false, false) => [0.0, 0.0, 0.0, 0.0],
}
}

View file

@ -1,6 +1,6 @@
// Copyright 2022 System76 <info@system76.com> // Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
pub mod column; pub mod list_column;
pub use self::column::{ListColumn, list_column}; pub use self::list_column::{ListButton, ListColumn, button, list_column};

View file

@ -15,8 +15,6 @@ use std::f32::consts::PI;
use std::time::Duration; use std::time::Duration;
const MIN_ANGLE: Radians = Radians(PI / 8.0); const MIN_ANGLE: Radians = Radians(PI / 8.0);
const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0);
const BASE_ROTATION_SPEED: u32 = u32::MAX / 80;
#[must_use] #[must_use]
pub struct Circular<Theme> pub struct Circular<Theme>
@ -83,6 +81,12 @@ where
self.progress = Some(progress.clamp(0.0, 1.0)); self.progress = Some(progress.clamp(0.0, 1.0));
self self
} }
fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) {
let cap_angle = self.bar_height / track_radius;
let gap = MIN_ANGLE.0.max(cap_angle);
(gap - cap_angle, 2.0 * PI - gap * 2.0)
}
} }
impl<Theme> Default for Circular<Theme> impl<Theme> Default for Circular<Theme>
@ -122,7 +126,7 @@ impl Default for Animation {
} }
impl Animation { impl Animation {
fn next(&self, additional_rotation: u32, now: Instant) -> Self { fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self {
match self { match self {
Self::Expanding { rotation, .. } => Self::Contracting { Self::Expanding { rotation, .. } => Self::Contracting {
start: now, start: now,
@ -133,9 +137,9 @@ impl Animation {
Self::Contracting { rotation, .. } => Self::Expanding { Self::Contracting { rotation, .. } => Self::Expanding {
start: now, start: now,
progress: 0.0, progress: 0.0,
rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( rotation: rotation.wrapping_add(
(f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32,
)), ),
last: now, last: now,
}, },
} }
@ -157,6 +161,7 @@ impl Animation {
&self, &self,
cycle_duration: Duration, cycle_duration: Duration,
rotation_duration: Duration, rotation_duration: Duration,
wrap_angle: f32,
now: Instant, now: Instant,
) -> Self { ) -> Self {
let elapsed = now.duration_since(self.start()); let elapsed = now.duration_since(self.start());
@ -165,7 +170,7 @@ impl Animation {
* (u32::MAX) as f32) as u32; * (u32::MAX) as f32) as u32;
match elapsed { match elapsed {
elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now),
_ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now),
} }
} }
@ -267,10 +272,13 @@ where
return; return;
} }
if let Event::Window(window::Event::RedrawRequested(now)) = event { if let Event::Window(window::Event::RedrawRequested(now)) = event {
state.animation = let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height);
state state.animation = state.animation.timed_transition(
.animation self.cycle_duration,
.timed_transition(self.cycle_duration, self.rotation_duration, *now); self.rotation_duration,
wrap_angle,
*now,
);
state.cache.clear(); state.cache.clear();
shell.request_redraw(); shell.request_redraw();
@ -380,22 +388,23 @@ where
} else { } else {
let mut builder = canvas::path::Builder::new(); let mut builder = canvas::path::Builder::new();
let start = Radians(state.animation.rotation() * 2.0 * PI); let start = state.animation.rotation() * 2.0 * PI;
let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius);
let (start_angle, end_angle) = match state.animation { let (start_angle, end_angle) = match state.animation {
Animation::Expanding { progress, .. } => ( Animation::Expanding { progress, .. } => (
start, start,
start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)), start + min_angle + wrap_angle * smootherstep(progress),
), ),
Animation::Contracting { progress, .. } => ( Animation::Contracting { progress, .. } => (
start + WRAP_ANGLE * (smootherstep(progress)), start + wrap_angle * smootherstep(progress),
start + MIN_ANGLE + WRAP_ANGLE, start + min_angle + wrap_angle,
), ),
}; };
builder.arc(canvas::path::Arc { builder.arc(canvas::path::Arc {
center: frame.center(), center: frame.center(),
radius: track_radius, radius: track_radius,
start_angle, start_angle: Radians(start_angle),
end_angle, end_angle: Radians(end_angle),
}); });
let bar_path = builder.build(); let bar_path = builder.build();
@ -410,23 +419,23 @@ where
let mut builder = canvas::path::Builder::new(); let mut builder = canvas::path::Builder::new();
// get center of end of arc for rounded cap // get center of end of arc for rounded cap
let end_center = frame.center() let end_center =
+ Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius;
builder.arc(canvas::path::Arc { builder.arc(canvas::path::Arc {
center: end_center, center: end_center,
radius: self.bar_height / 2.0, radius: self.bar_height / 2.0,
start_angle: Radians(end_angle.0), start_angle: Radians(end_angle),
end_angle: Radians(end_angle.0 + PI), end_angle: Radians(end_angle + PI),
}); });
// get center of start of arc for rounded cap // get center of start of arc for rounded cap
let start_center = frame.center() let start_center = frame.center()
+ Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius;
builder.arc(canvas::path::Arc { builder.arc(canvas::path::Arc {
center: start_center, center: start_center,
radius: self.bar_height / 2.0, radius: self.bar_height / 2.0,
start_angle: Radians(start_angle.0 - PI), start_angle: Radians(start_angle - PI),
end_angle: Radians(start_angle.0), end_angle: Radians(start_angle),
}); });
let cap_path = builder.build(); let cap_path = builder.build();

View file

@ -1,5 +1,5 @@
//! Create choices using radio buttons. //! Create choices using radio buttons.
use crate::Theme; use crate::{Theme, theme};
use iced::border; use iced::border;
use iced_core::event::{self, Event}; use iced_core::event::{self, Event};
use iced_core::layout; use iced_core::layout;
@ -92,7 +92,7 @@ where
{ {
is_selected: bool, is_selected: bool,
on_click: Message, on_click: Message,
label: Element<'a, Message, Theme, Renderer>, label: Option<Element<'a, Message, Theme, Renderer>>,
width: Length, width: Length,
size: f32, size: f32,
spacing: f32, spacing: f32,
@ -106,9 +106,6 @@ where
/// The default size of a [`Radio`] button. /// The default size of a [`Radio`] button.
pub const DEFAULT_SIZE: f32 = 16.0; pub const DEFAULT_SIZE: f32 = 16.0;
/// The default spacing of a [`Radio`] button.
pub const DEFAULT_SPACING: f32 = 8.0;
/// Creates a new [`Radio`] button. /// Creates a new [`Radio`] button.
/// ///
/// It expects: /// It expects:
@ -126,10 +123,29 @@ where
Radio { Radio {
is_selected: Some(value) == selected, is_selected: Some(value) == selected,
on_click: f(value), on_click: f(value),
label: label.into(), label: Some(label.into()),
width: Length::Shrink, width: Length::Shrink,
size: Self::DEFAULT_SIZE, size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING, spacing: theme::spacing().space_xs as f32,
}
}
/// Creates a new [`Radio`] button without a label.
///
/// This is intended for internal use with the settings item builder,
/// where the label comes from the settings item title instead.
pub(crate) fn new_no_label<V, F>(value: V, selected: Option<V>, f: F) -> Self
where
V: Eq + Copy,
F: FnOnce(V) -> Message,
{
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
label: None,
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: theme::spacing().space_xs as f32,
} }
} }
@ -161,11 +177,17 @@ where
Renderer: iced_core::Renderer, Renderer: iced_core::Renderer,
{ {
fn children(&self) -> Vec<Tree> { fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.label)] if let Some(label) = &self.label {
vec![Tree::new(label)]
} else {
vec![]
}
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(std::slice::from_mut(&mut self.label)); if let Some(label) = &mut self.label {
tree.diff_children(std::slice::from_mut(label));
}
} }
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size { Size {
@ -180,16 +202,20 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout::next_to_each_other( if let Some(label) = &mut self.label {
&limits.width(self.width), layout::next_to_each_other(
self.spacing, &limits.width(self.width),
|_| layout::Node::new(Size::new(self.size, self.size)), self.spacing,
|limits| { |_| layout::Node::new(Size::new(self.size, self.size)),
self.label |limits| {
.as_widget_mut() label
.layout(&mut tree.children[0], renderer, limits) .as_widget_mut()
}, .layout(&mut tree.children[0], renderer, limits)
) },
)
} else {
layout::Node::new(Size::new(self.size, self.size))
}
} }
fn operate( fn operate(
@ -199,12 +225,14 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<()>, operation: &mut dyn iced_core::widget::Operation<()>,
) { ) {
self.label.as_widget_mut().operate( if let Some(label) = &mut self.label {
&mut tree.children[0], label.as_widget_mut().operate(
layout.children().nth(1).unwrap(), &mut tree.children[0],
renderer, layout.children().nth(1).unwrap(),
operation, renderer,
); operation,
);
}
} }
fn update( fn update(
@ -218,24 +246,25 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
self.label.as_widget_mut().update( if let Some(label) = &mut self.label {
&mut tree.children[0], label.as_widget_mut().update(
event, &mut tree.children[0],
layout.children().nth(1).unwrap(), event,
cursor, layout.children().nth(1).unwrap(),
renderer, cursor,
clipboard, renderer,
shell, clipboard,
viewport, shell,
); viewport,
);
}
if !shell.is_event_captured() { if !shell.is_event_captured() {
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerLifted { .. }) => {
if cursor.is_over(layout.bounds()) { if cursor.is_over(layout.bounds()) {
shell.publish(self.on_click.clone()); shell.publish(self.on_click.clone());
shell.capture_event(); shell.capture_event();
return; return;
} }
@ -253,13 +282,17 @@ where
viewport: &Rectangle, viewport: &Rectangle,
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
let interaction = self.label.as_widget().mouse_interaction( let interaction = if let Some(label) = &self.label {
&tree.children[0], label.as_widget().mouse_interaction(
layout.children().nth(1).unwrap(), &tree.children[0],
cursor, layout.children().nth(1).unwrap(),
viewport, cursor,
renderer, viewport,
); renderer,
)
} else {
mouse::Interaction::default()
};
if interaction == mouse::Interaction::default() { if interaction == mouse::Interaction::default() {
if cursor.is_over(layout.bounds()) { if cursor.is_over(layout.bounds()) {
@ -284,8 +317,6 @@ where
) { ) {
let is_mouse_over = cursor.is_over(layout.bounds()); let is_mouse_over = cursor.is_over(layout.bounds());
let mut children = layout.children();
let custom_style = if is_mouse_over { let custom_style = if is_mouse_over {
theme.style( theme.style(
&(), &(),
@ -302,16 +333,21 @@ where
) )
}; };
{ let (dot_bounds, label_layout) = if self.label.is_some() {
let layout = children.next().unwrap(); let mut children = layout.children();
let bounds = layout.bounds(); let dot_bounds = children.next().unwrap().bounds();
(dot_bounds, children.next())
} else {
(layout.bounds(), None)
};
let size = bounds.width; {
let size = dot_bounds.width;
let dot_size = 6.0; let dot_size = 6.0;
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds: dot_bounds,
border: Border { border: Border {
radius: (size / 2.0).into(), radius: (size / 2.0).into(),
width: custom_style.border_width, width: custom_style.border_width,
@ -326,8 +362,8 @@ where
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: Rectangle {
x: bounds.x + (size - dot_size) / 2.0, x: dot_bounds.x + (size - dot_size) / 2.0,
y: bounds.y + (size - dot_size) / 2.0, y: dot_bounds.y + (size - dot_size) / 2.0,
width: dot_size, width: dot_size,
height: dot_size, height: dot_size,
}, },
@ -339,9 +375,8 @@ where
} }
} }
{ if let (Some(label), Some(label_layout)) = (&self.label, label_layout) {
let label_layout = children.next().unwrap(); label.as_widget().draw(
self.label.as_widget().draw(
&tree.children[0], &tree.children[0],
renderer, renderer,
theme, theme,
@ -361,7 +396,7 @@ where
viewport: &Rectangle, viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.label.as_widget_mut().overlay( self.label.as_mut()?.as_widget_mut().overlay(
&mut tree.children[0], &mut tree.children[0],
layout.children().nth(1).unwrap(), layout.children().nth(1).unwrap(),
renderer, renderer,
@ -377,12 +412,14 @@ where
renderer: &Renderer, renderer: &Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) { ) {
self.label.as_widget().drag_destinations( if let Some(label) = &self.label {
&state.children[0], label.as_widget().drag_destinations(
layout.children().nth(1).unwrap(), &state.children[0],
renderer, layout.children().nth(1).unwrap(),
dnd_rectangles, renderer,
); dnd_rectangles,
);
}
} }
} }

View file

@ -5,7 +5,7 @@ use std::borrow::Cow;
use crate::{ use crate::{
Element, Theme, theme, Element, Theme, theme,
widget::{FlexRow, Row, column, container, flex_row, row, text}, widget::{FlexRow, Row, column, container, flex_row, list, row, text},
}; };
use derive_setters::Setters; use derive_setters::Setters;
use iced_core::{Length, text::Wrapping}; use iced_core::{Length, text::Wrapping};
@ -103,7 +103,7 @@ pub struct Item<'a, Message> {
icon: Option<Element<'a, Message>>, icon: Option<Element<'a, Message>>,
} }
impl<'a, Message: 'static> Item<'a, Message> { impl<'a, Message: Clone + 'static> Item<'a, Message> {
/// Assigns a control to the item. /// Assigns a control to the item.
pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> { pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
item_row(self.control_(widget.into())) item_row(self.control_(widget.into()))
@ -114,39 +114,109 @@ impl<'a, Message: 'static> Item<'a, Message> {
flex_item_row(self.control_(widget.into())) flex_item_row(self.control_(widget.into()))
} }
#[inline(never)] fn label(self) -> Element<'a, Message> {
fn control_(self, widget: Element<'a, Message>) -> Vec<Element<'a, Message>> {
let mut contents = Vec::with_capacity(4);
if let Some(icon) = self.icon {
contents.push(icon);
}
if let Some(description) = self.description { if let Some(description) = self.description {
let column = column::with_capacity(2) column::with_capacity(2)
.spacing(2) .spacing(2)
.push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::body(self.title).wrapping(Wrapping::Word))
.push(text::caption(description).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word))
.width(Length::Fill); .width(Length::Fill)
.into()
contents.push(column.into());
} else { } else {
contents.push(text(self.title).width(Length::Fill).into()); text(self.title).width(Length::Fill).into()
} }
}
#[inline(never)]
fn control_(mut self, widget: Element<'a, Message>) -> Vec<Element<'a, Message>> {
let mut contents = Vec::with_capacity(3);
if let Some(icon) = self.icon.take() {
contents.push(icon);
}
contents.push(self.label());
contents.push(widget); contents.push(widget);
contents contents
} }
fn control_start(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
item_row(vec![widget.into(), self.label()])
}
pub fn toggler( pub fn toggler(
self, self,
is_checked: bool, is_checked: bool,
message: impl Fn(bool) -> Message + 'static, message: impl Fn(bool) -> Message + 'static,
) -> Row<'a, Message, Theme> { ) -> list::ListButton<'a, Message> {
self.control( let on_press = message(!is_checked);
crate::widget::toggler(is_checked) list::button(
.width(Length::Shrink) self.control(
.on_toggle(message), crate::widget::toggler(is_checked)
.width(Length::Shrink)
.on_toggle(message),
),
) )
.on_press(on_press)
}
pub fn toggler_maybe(
self,
is_checked: bool,
message: Option<impl Fn(bool) -> Message + 'static>,
) -> list::ListButton<'a, Message> {
let on_press = message.as_ref().map(|f| f(!is_checked));
list::button(
self.control(
crate::widget::toggler(is_checked)
.width(Length::Shrink)
.on_toggle_maybe(message),
),
)
.on_press_maybe(on_press)
}
pub fn checkbox(
self,
is_checked: bool,
message: impl Fn(bool) -> Message + 'static,
) -> list::ListButton<'a, Message> {
let on_press = message(!is_checked);
list::button(
self.control_start(
crate::widget::checkbox(is_checked)
.width(Length::Shrink)
.on_toggle(message),
),
)
.on_press(on_press)
}
pub fn checkbox_maybe(
self,
is_checked: bool,
message: Option<impl Fn(bool) -> Message + 'static>,
) -> list::ListButton<'a, Message> {
let on_press = message.as_ref().map(|f| f(!is_checked));
list::button(
self.control_start(
crate::widget::checkbox(is_checked)
.width(Length::Shrink)
.on_toggle_maybe(message),
),
)
.on_press_maybe(on_press)
}
pub fn radio<V, F>(self, value: V, selected: Option<V>, f: F) -> list::ListButton<'a, Message>
where
V: Eq + Copy,
F: Fn(V) -> Message,
{
let on_press = f(value);
list::button(
self.control_start(crate::widget::radio::Radio::new_no_label(
value, selected, f,
)),
)
.on_press(on_press)
} }
} }

View file

@ -2,16 +2,24 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::Element; use crate::Element;
use crate::widget::{ListColumn, column, text}; use crate::widget::list_column::IntoListItem;
use crate::widget::{ListColumn, column, list_column, text};
use std::borrow::Cow; use std::borrow::Cow;
/// A section within a settings view column. /// A section within a settings view column.
pub fn section<'a, Message: 'static>() -> Section<'a, Message> { pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> {
with_column(ListColumn::default()) with_column(ListColumn::default())
} }
/// A section with a pre-defined list column of a given capacity.
pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> {
with_column(list_column::with_capacity(capacity))
}
/// A section with a pre-defined list column. /// A section with a pre-defined list column.
pub fn with_column<Message: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> { pub fn with_column<Message: Clone + 'static>(
children: ListColumn<'_, Message>,
) -> Section<'_, Message> {
Section { Section {
header: None, header: None,
children, children,
@ -24,9 +32,9 @@ pub struct Section<'a, Message> {
children: ListColumn<'a, Message>, children: ListColumn<'a, Message>,
} }
impl<'a, Message: 'static> Section<'a, Message> { impl<'a, Message: Clone + 'static> Section<'a, Message> {
/// Define an optional title for the section. /// Define an optional title for the section.
pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self { pub fn title(self, title: impl Into<Cow<'a, str>>) -> Self {
self.header(text::heading(title.into())) self.header(text::heading(title.into()))
} }
@ -38,13 +46,13 @@ impl<'a, Message: 'static> Section<'a, Message> {
/// Add a child element to the section's list column. /// Add a child element to the section's list column.
#[allow(clippy::should_implement_trait)] #[allow(clippy::should_implement_trait)]
pub fn add(mut self, item: impl Into<Element<'a, Message>>) -> Self { pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self {
self.children = self.children.add(item.into()); self.children = self.children.add(item);
self self
} }
/// Add a child element to the section's list column, if `Some`. /// Add a child element to the section's list column, if `Some`.
pub fn add_maybe(self, item: Option<impl Into<Element<'a, Message>>>) -> Self { pub fn add_maybe(self, item: Option<impl IntoListItem<'a, Message>>) -> Self {
if let Some(item) = item { if let Some(item) = item {
self.add(item) self.add(item)
} else { } else {
@ -55,13 +63,13 @@ impl<'a, Message: 'static> Section<'a, Message> {
/// Extends the [`Section`] with the given children. /// Extends the [`Section`] with the given children.
pub fn extend( pub fn extend(
self, self,
children: impl IntoIterator<Item = impl Into<Element<'a, Message>>>, children: impl IntoIterator<Item = impl IntoListItem<'a, Message>>,
) -> Self { ) -> Self {
children.into_iter().fold(self, Self::add) children.into_iter().fold(self, Self::add)
} }
} }
impl<'a, Message: 'static> From<Section<'a, Message>> for Element<'a, Message> { impl<'a, Message: Clone + 'static> From<Section<'a, Message>> for Element<'a, Message> {
fn from(data: Section<'a, Message>) -> Self { fn from(data: Section<'a, Message>) -> Self {
column::with_capacity(2) column::with_capacity(2)
.spacing(8) .spacing(8)

View file

@ -161,7 +161,10 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::default()) tree::State::new(State {
prev_toggled: self.is_toggled,
..State::default()
})
} }
fn id(&self) -> Option<Id> { fn id(&self) -> Option<Id> {
@ -238,6 +241,14 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
return; return;
}; };
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
// animate external changes
if state.prev_toggled != self.is_toggled {
state.anim.changed(self.duration);
shell.request_redraw();
state.prev_toggled = self.is_toggled;
}
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -246,6 +257,7 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
if mouse_over { if mouse_over {
shell.publish((on_toggle)(!self.is_toggled)); shell.publish((on_toggle)(!self.is_toggled));
state.anim.changed(self.duration); state.anim.changed(self.duration);
state.prev_toggled = !self.is_toggled;
shell.capture_event(); shell.capture_event();
} }
} }
@ -430,4 +442,5 @@ pub fn next_to_each_other(
pub struct State { pub struct State {
text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>, text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
anim: anim::State, anim: anim::State,
prev_toggled: bool,
} }