Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95756b1a57 | ||
|
|
c423ad1bfc | ||
|
|
8d7bcab258 | ||
|
|
c162a1f24a | ||
|
|
3f9e93067b | ||
|
|
917af9fda2 | ||
|
|
9b465a8b5c | ||
|
|
9cac422c24 | ||
|
|
0fc4638af3 | ||
|
|
3d8d8915be | ||
|
|
46d9f0c344 | ||
|
|
0d69cd9183 | ||
|
|
52116d2f36 | ||
|
|
0e72508dcc | ||
|
|
1d7113a244 | ||
|
|
e287a789c1 | ||
|
|
6caccaba33 |
26 changed files with 643 additions and 325 deletions
10
Cargo.toml
10
Cargo.toml
|
|
@ -58,6 +58,7 @@ desktop = [
|
|||
"process",
|
||||
"dep:cosmic-settings-config",
|
||||
"dep:freedesktop-desktop-entry",
|
||||
"dep:image-extras",
|
||||
"dep:mime",
|
||||
"dep:shlex",
|
||||
"tokio?/io-util",
|
||||
|
|
@ -141,9 +142,14 @@ css-color = "0.2.8"
|
|||
derive_setters = "0.1.9"
|
||||
futures = "0.3"
|
||||
image = { version = "0.25.10", default-features = false, features = [
|
||||
"ico",
|
||||
"jpeg",
|
||||
"png",
|
||||
] }
|
||||
image-extras = { version = "0.1.0", default-features = false, features = [
|
||||
"xpm",
|
||||
"xbm",
|
||||
], optional = true }
|
||||
libc = { version = "0.2.183", optional = true }
|
||||
log = "0.4"
|
||||
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" }
|
||||
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-desktop-entry = { version = "0.8.1", 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.
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
|
||||
|
|
|
|||
4
build.rs
4
build.rs
|
|
@ -3,7 +3,9 @@ use std::env;
|
|||
fn main() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ features = [
|
|||
"single-instance",
|
||||
"surface-message",
|
||||
"multi-window",
|
||||
"wgpu",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ impl cosmic::Application for App {
|
|||
.map_or("No page selected", String::as_str);
|
||||
|
||||
let centered = widget::container(
|
||||
widget::column::with_capacity(5)
|
||||
widget::column::with_capacity(14)
|
||||
.push(widget::text::body(page_content))
|
||||
.push(
|
||||
widget::text_input::text_input("", &self.input_1)
|
||||
|
|
@ -223,6 +223,7 @@ impl cosmic::Application for App {
|
|||
.on_clear(Message::Ignore),
|
||||
)
|
||||
.push(widget::progress_bar::circular::Circular::new().size(50.0))
|
||||
.push(widget::progress_bar::circular::Circular::new().size(20.0))
|
||||
.push(
|
||||
widget::progress_bar::linear::Linear::new()
|
||||
.girth(10.0)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ links = Links
|
|||
developers = Entwickler(innen)
|
||||
designers = Designer(innen)
|
||||
artists = Künstler(innen)
|
||||
translators = Übersetzer*innen
|
||||
translators = Übersetzer(innen)
|
||||
documenters = Dokumentierer(innen)
|
||||
# Calendar
|
||||
january = Januar { $year }
|
||||
|
|
|
|||
0
i18n/eu/libcosmic.ftl
Normal file
0
i18n/eu/libcosmic.ftl
Normal 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
|
||||
|
|
@ -2,26 +2,33 @@ february = { $year }년 2월
|
|||
close = 닫기
|
||||
documenters = 문서 작성자
|
||||
november = { $year }년 11월
|
||||
friday = 금
|
||||
tuesday = 화
|
||||
friday = 금요일
|
||||
tuesday = 화요일
|
||||
may = { $year }년 5월
|
||||
wednesday = 수
|
||||
wednesday = 수요일
|
||||
april = { $year }년 4월
|
||||
monday = 월
|
||||
monday = 월요일
|
||||
translators = 번역가
|
||||
artists = 아티스트
|
||||
license = 라이선스
|
||||
december = { $year }년 12월
|
||||
sunday = 일
|
||||
sunday = 일요일
|
||||
links = 링크
|
||||
march = { $year }년 3월
|
||||
june = { $year }년 6월
|
||||
saturday = 토
|
||||
saturday = 토요일
|
||||
august = { $year }년 8월
|
||||
developers = 개발자
|
||||
july = { $year }년 7월
|
||||
thursday = 목
|
||||
thursday = 목요일
|
||||
september = { $year }년 9월
|
||||
designers = 디자이너
|
||||
october = { $year }년 10월
|
||||
january = { $year }년 1월
|
||||
mon = 월
|
||||
tue = 화
|
||||
wed = 수
|
||||
thu = 목
|
||||
fri = 금
|
||||
sat = 토
|
||||
sun = 일
|
||||
|
|
|
|||
|
|
@ -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
2
iced
|
|
@ -1 +1 @@
|
|||
Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1
|
||||
Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece
|
||||
|
|
@ -128,6 +128,9 @@ impl<A: crate::app::Application> BootFn<cosmic::Cosmic<A>, crate::Action<A::Mess
|
|||
///
|
||||
/// Returns error on application failure.
|
||||
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")))]
|
||||
if let Some(threshold) = settings.default_mmap_threshold {
|
||||
crate::malloc::limit_mmap_threshold(threshold);
|
||||
|
|
@ -194,6 +197,9 @@ where
|
|||
App::Flags: CosmicFlags,
|
||||
App::Message: Clone + std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
#[cfg(feature = "desktop")]
|
||||
image_extras::register();
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ impl DefaultStyle for Theme {
|
|||
fn default_style(&self) -> Appearance {
|
||||
let cosmic = self.cosmic();
|
||||
Appearance {
|
||||
icon_color: cosmic.bg_color().into(),
|
||||
icon_color: cosmic.on_bg_color().into(),
|
||||
background_color: cosmic.bg_color().into(),
|
||||
text_color: cosmic.on_bg_color().into(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ pub enum Button {
|
|||
IconVertical,
|
||||
Image,
|
||||
Link,
|
||||
ListItem,
|
||||
ListItem([f32; 4]),
|
||||
MenuFolder,
|
||||
MenuItem,
|
||||
MenuRoot,
|
||||
|
|
@ -148,8 +148,8 @@ pub fn appearance(
|
|||
appearance.text_color = Some(component.on.into());
|
||||
corner_radii = &cosmic.corner_radii.radius_s;
|
||||
}
|
||||
Button::ListItem => {
|
||||
corner_radii = &[0.0; 4];
|
||||
Button::ListItem(radii) => {
|
||||
corner_radii = radii;
|
||||
let (background, text, icon) = color(&cosmic.background.component);
|
||||
|
||||
if selected {
|
||||
|
|
@ -197,7 +197,7 @@ impl Catalog for crate::Theme {
|
|||
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!(
|
||||
style,
|
||||
Button::Icon | Button::IconVertical | Button::HeaderBar
|
||||
|
|
@ -209,7 +209,15 @@ impl Catalog for crate::Theme {
|
|||
};
|
||||
|
||||
(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 {
|
||||
|
|
@ -237,7 +245,7 @@ impl Catalog for crate::Theme {
|
|||
return hovered(focused, self);
|
||||
}
|
||||
|
||||
appearance(
|
||||
let mut s = appearance(
|
||||
self,
|
||||
focused || matches!(style, Button::Image),
|
||||
selected,
|
||||
|
|
@ -256,7 +264,15 @@ impl Catalog for crate::Theme {
|
|||
|
||||
(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 {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ pub mod application {
|
|||
iced::theme::Style {
|
||||
background_color: cosmic.bg_color().into(),
|
||||
text_color: cosmic.on_bg_color().into(),
|
||||
icon_color: cosmic.bg_color().into(),
|
||||
icon_color: cosmic.on_bg_color().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{
|
||||
Apply, Element, fl,
|
||||
iced::{Alignment, Length},
|
||||
widget::{self, space},
|
||||
widget::{self, list},
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Default, Clone, derive_setters::Setters)]
|
||||
#[setters(into, strip_option)]
|
||||
|
|
@ -104,19 +105,23 @@ pub fn about<'a, Message: Clone + 'static>(
|
|||
space_xxs, space_m, ..
|
||||
} = crate::theme::spacing();
|
||||
|
||||
let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> {
|
||||
widget::row::with_capacity(3)
|
||||
.push(widget::text(name))
|
||||
.push(space::horizontal())
|
||||
let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style {
|
||||
color: Some(theme.cosmic().accent_text_color().into()),
|
||||
});
|
||||
|
||||
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(
|
||||
(!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)
|
||||
.apply(widget::button::custom)
|
||||
.class(crate::theme::Button::Link)
|
||||
.apply(list::button)
|
||||
.on_press(on_url_press(url))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
};
|
||||
|
||||
let section = |list: &'a Vec<(String, String)>, title: String| {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer;
|
|||
use iced_core::mouse::Cursor;
|
||||
use iced_core::widget::{Tree, tree};
|
||||
use iced_core::{
|
||||
Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||
event, layout, renderer, window,
|
||||
Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size,
|
||||
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::codecs::gif::GifDecoder;
|
||||
use image_rs::codecs::png::PngDecoder;
|
||||
|
|
@ -146,7 +146,7 @@ impl Frames {
|
|||
|
||||
match image_type {
|
||||
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))?),
|
||||
}
|
||||
}
|
||||
|
|
@ -168,10 +168,10 @@ impl Frames {
|
|||
let first = frames.first().cloned().unwrap();
|
||||
let total_bytes = frames
|
||||
.iter()
|
||||
.map(|f| match f.handle.data() {
|
||||
iced_core::image::Handle::Path(..) => 0,
|
||||
iced_core::image::Handle::Bytes(_, b) => b.len(),
|
||||
iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(),
|
||||
.map(|f| match &f.handle {
|
||||
Handle::Path(..) => 0,
|
||||
Handle::Bytes(_, b) => b.len(),
|
||||
Handle::Rgba { pixels, .. } => pixels.len(),
|
||||
})
|
||||
.sum::<usize>()
|
||||
.try_into()
|
||||
|
|
@ -324,7 +324,11 @@ where
|
|||
&self.frames.first.handle,
|
||||
self.width,
|
||||
self.height,
|
||||
None,
|
||||
self.content_fit,
|
||||
Rotation::default(),
|
||||
false,
|
||||
[0.0; 4],
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -371,37 +375,18 @@ where
|
|||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
// Pulled from iced_native::widget::<Image as Widget>::draw
|
||||
//
|
||||
// TODO: export iced_native::widget::image::draw as standalone function
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(&state.current.frame.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
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);
|
||||
}
|
||||
}
|
||||
iced_widget::image::draw(
|
||||
renderer,
|
||||
layout,
|
||||
&state.current.frame.handle,
|
||||
None,
|
||||
iced_core::border::Radius::default(),
|
||||
self.content_fit,
|
||||
FilterMethod::default(),
|
||||
Rotation::default(),
|
||||
1.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
//! Embedded icons for platforms which do not support icon themes yet.
|
||||
|
||||
/// 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> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(any(not(unix), target_os = "macos"))]
|
||||
/// Get a bundled icon on non-unix platforms.
|
||||
pub fn get(icon_name: &str) -> Option<super::Data> {
|
||||
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)))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(any(not(unix), target_os = "macos"))]
|
||||
include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs"));
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ impl Named {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[must_use]
|
||||
pub fn path(self) -> Option<PathBuf> {
|
||||
let name = &*self.name;
|
||||
|
|
@ -107,7 +107,7 @@ impl Named {
|
|||
result
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[cfg(any(not(unix), target_os = "macos"))]
|
||||
#[must_use]
|
||||
pub fn path(self) -> Option<PathBuf> {
|
||||
//TODO: implement icon lookup for Windows
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
213
src/widget/list/list_column.rs
Normal file
213
src/widget/list/list_column.rs
Normal 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],
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// 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};
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ use std::f32::consts::PI;
|
|||
use std::time::Duration;
|
||||
|
||||
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]
|
||||
pub struct Circular<Theme>
|
||||
|
|
@ -83,6 +81,12 @@ where
|
|||
self.progress = Some(progress.clamp(0.0, 1.0));
|
||||
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>
|
||||
|
|
@ -122,7 +126,7 @@ impl Default for 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 {
|
||||
Self::Expanding { rotation, .. } => Self::Contracting {
|
||||
start: now,
|
||||
|
|
@ -133,9 +137,9 @@ impl Animation {
|
|||
Self::Contracting { rotation, .. } => Self::Expanding {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add(
|
||||
(f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32,
|
||||
)),
|
||||
rotation: rotation.wrapping_add(
|
||||
(f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32,
|
||||
),
|
||||
last: now,
|
||||
},
|
||||
}
|
||||
|
|
@ -157,6 +161,7 @@ impl Animation {
|
|||
&self,
|
||||
cycle_duration: Duration,
|
||||
rotation_duration: Duration,
|
||||
wrap_angle: f32,
|
||||
now: Instant,
|
||||
) -> Self {
|
||||
let elapsed = now.duration_since(self.start());
|
||||
|
|
@ -165,7 +170,7 @@ impl Animation {
|
|||
* (u32::MAX) as f32) as u32;
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
@ -267,10 +272,13 @@ where
|
|||
return;
|
||||
}
|
||||
if let Event::Window(window::Event::RedrawRequested(now)) = event {
|
||||
state.animation =
|
||||
state
|
||||
.animation
|
||||
.timed_transition(self.cycle_duration, self.rotation_duration, *now);
|
||||
let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height);
|
||||
state.animation = state.animation.timed_transition(
|
||||
self.cycle_duration,
|
||||
self.rotation_duration,
|
||||
wrap_angle,
|
||||
*now,
|
||||
);
|
||||
|
||||
state.cache.clear();
|
||||
shell.request_redraw();
|
||||
|
|
@ -380,22 +388,23 @@ where
|
|||
} else {
|
||||
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 {
|
||||
Animation::Expanding { progress, .. } => (
|
||||
start,
|
||||
start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)),
|
||||
start + min_angle + wrap_angle * smootherstep(progress),
|
||||
),
|
||||
Animation::Contracting { progress, .. } => (
|
||||
start + WRAP_ANGLE * (smootherstep(progress)),
|
||||
start + MIN_ANGLE + WRAP_ANGLE,
|
||||
start + wrap_angle * smootherstep(progress),
|
||||
start + min_angle + wrap_angle,
|
||||
),
|
||||
};
|
||||
builder.arc(canvas::path::Arc {
|
||||
center: frame.center(),
|
||||
radius: track_radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
start_angle: Radians(start_angle),
|
||||
end_angle: Radians(end_angle),
|
||||
});
|
||||
|
||||
let bar_path = builder.build();
|
||||
|
|
@ -410,23 +419,23 @@ where
|
|||
let mut builder = canvas::path::Builder::new();
|
||||
|
||||
// get center of end of arc for rounded cap
|
||||
let end_center = frame.center()
|
||||
+ Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius;
|
||||
let end_center =
|
||||
frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius;
|
||||
builder.arc(canvas::path::Arc {
|
||||
center: end_center,
|
||||
radius: self.bar_height / 2.0,
|
||||
start_angle: Radians(end_angle.0),
|
||||
end_angle: Radians(end_angle.0 + PI),
|
||||
start_angle: Radians(end_angle),
|
||||
end_angle: Radians(end_angle + PI),
|
||||
});
|
||||
|
||||
// get center of start of arc for rounded cap
|
||||
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 {
|
||||
center: start_center,
|
||||
radius: self.bar_height / 2.0,
|
||||
start_angle: Radians(start_angle.0 - PI),
|
||||
end_angle: Radians(start_angle.0),
|
||||
start_angle: Radians(start_angle - PI),
|
||||
end_angle: Radians(start_angle),
|
||||
});
|
||||
|
||||
let cap_path = builder.build();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Create choices using radio buttons.
|
||||
use crate::Theme;
|
||||
use crate::{Theme, theme};
|
||||
use iced::border;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout;
|
||||
|
|
@ -92,7 +92,7 @@ where
|
|||
{
|
||||
is_selected: bool,
|
||||
on_click: Message,
|
||||
label: Element<'a, Message, Theme, Renderer>,
|
||||
label: Option<Element<'a, Message, Theme, Renderer>>,
|
||||
width: Length,
|
||||
size: f32,
|
||||
spacing: f32,
|
||||
|
|
@ -106,9 +106,6 @@ where
|
|||
/// The default size of a [`Radio`] button.
|
||||
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.
|
||||
///
|
||||
/// It expects:
|
||||
|
|
@ -126,10 +123,29 @@ where
|
|||
Radio {
|
||||
is_selected: Some(value) == selected,
|
||||
on_click: f(value),
|
||||
label: label.into(),
|
||||
label: Some(label.into()),
|
||||
width: Length::Shrink,
|
||||
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,
|
||||
{
|
||||
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) {
|
||||
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> {
|
||||
Size {
|
||||
|
|
@ -180,16 +202,20 @@ where
|
|||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::next_to_each_other(
|
||||
&limits.width(self.width),
|
||||
self.spacing,
|
||||
|_| layout::Node::new(Size::new(self.size, self.size)),
|
||||
|limits| {
|
||||
self.label
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
},
|
||||
)
|
||||
if let Some(label) = &mut self.label {
|
||||
layout::next_to_each_other(
|
||||
&limits.width(self.width),
|
||||
self.spacing,
|
||||
|_| layout::Node::new(Size::new(self.size, self.size)),
|
||||
|limits| {
|
||||
label
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
layout::Node::new(Size::new(self.size, self.size))
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
|
|
@ -199,12 +225,14 @@ where
|
|||
renderer: &Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation<()>,
|
||||
) {
|
||||
self.label.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().nth(1).unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
if let Some(label) = &mut self.label {
|
||||
label.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().nth(1).unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
|
|
@ -218,24 +246,25 @@ where
|
|||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.label.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout.children().nth(1).unwrap(),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
if let Some(label) = &mut self.label {
|
||||
label.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout.children().nth(1).unwrap(),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
if !shell.is_event_captured() {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
shell.publish(self.on_click.clone());
|
||||
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
|
|
@ -253,13 +282,17 @@ where
|
|||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let interaction = self.label.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout.children().nth(1).unwrap(),
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
let interaction = if let Some(label) = &self.label {
|
||||
label.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout.children().nth(1).unwrap(),
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
};
|
||||
|
||||
if interaction == mouse::Interaction::default() {
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
|
|
@ -284,8 +317,6 @@ where
|
|||
) {
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
|
||||
let mut children = layout.children();
|
||||
|
||||
let custom_style = if is_mouse_over {
|
||||
theme.style(
|
||||
&(),
|
||||
|
|
@ -302,16 +333,21 @@ where
|
|||
)
|
||||
};
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
let (dot_bounds, label_layout) = if self.label.is_some() {
|
||||
let mut children = layout.children();
|
||||
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;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
bounds: dot_bounds,
|
||||
border: Border {
|
||||
radius: (size / 2.0).into(),
|
||||
width: custom_style.border_width,
|
||||
|
|
@ -326,8 +362,8 @@ where
|
|||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + (size - dot_size) / 2.0,
|
||||
y: bounds.y + (size - dot_size) / 2.0,
|
||||
x: dot_bounds.x + (size - dot_size) / 2.0,
|
||||
y: dot_bounds.y + (size - dot_size) / 2.0,
|
||||
width: dot_size,
|
||||
height: dot_size,
|
||||
},
|
||||
|
|
@ -339,9 +375,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
let label_layout = children.next().unwrap();
|
||||
self.label.as_widget().draw(
|
||||
if let (Some(label), Some(label_layout)) = (&self.label, label_layout) {
|
||||
label.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
|
|
@ -361,7 +396,7 @@ where
|
|||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> 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],
|
||||
layout.children().nth(1).unwrap(),
|
||||
renderer,
|
||||
|
|
@ -377,12 +412,14 @@ where
|
|||
renderer: &Renderer,
|
||||
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
|
||||
) {
|
||||
self.label.as_widget().drag_destinations(
|
||||
&state.children[0],
|
||||
layout.children().nth(1).unwrap(),
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
if let Some(label) = &self.label {
|
||||
label.as_widget().drag_destinations(
|
||||
&state.children[0],
|
||||
layout.children().nth(1).unwrap(),
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::borrow::Cow;
|
|||
|
||||
use crate::{
|
||||
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 iced_core::{Length, text::Wrapping};
|
||||
|
|
@ -103,7 +103,7 @@ pub struct Item<'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.
|
||||
pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
|
||||
item_row(self.control_(widget.into()))
|
||||
|
|
@ -114,39 +114,109 @@ impl<'a, Message: 'static> Item<'a, Message> {
|
|||
flex_item_row(self.control_(widget.into()))
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
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);
|
||||
}
|
||||
|
||||
fn label(self) -> Element<'a, Message> {
|
||||
if let Some(description) = self.description {
|
||||
let column = column::with_capacity(2)
|
||||
column::with_capacity(2)
|
||||
.spacing(2)
|
||||
.push(text::body(self.title).wrapping(Wrapping::Word))
|
||||
.push(text::caption(description).wrapping(Wrapping::Word))
|
||||
.width(Length::Fill);
|
||||
|
||||
contents.push(column.into());
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} 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
|
||||
}
|
||||
|
||||
fn control_start(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
|
||||
item_row(vec![widget.into(), self.label()])
|
||||
}
|
||||
|
||||
pub fn toggler(
|
||||
self,
|
||||
is_checked: bool,
|
||||
message: impl Fn(bool) -> Message + 'static,
|
||||
) -> Row<'a, Message, Theme> {
|
||||
self.control(
|
||||
crate::widget::toggler(is_checked)
|
||||
.width(Length::Shrink)
|
||||
.on_toggle(message),
|
||||
) -> list::ListButton<'a, Message> {
|
||||
let on_press = message(!is_checked);
|
||||
list::button(
|
||||
self.control(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,24 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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;
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn with_column<Message: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> {
|
||||
pub fn with_column<Message: Clone + 'static>(
|
||||
children: ListColumn<'_, Message>,
|
||||
) -> Section<'_, Message> {
|
||||
Section {
|
||||
header: None,
|
||||
children,
|
||||
|
|
@ -24,9 +32,9 @@ pub struct Section<'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.
|
||||
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()))
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +46,13 @@ impl<'a, Message: 'static> Section<'a, Message> {
|
|||
|
||||
/// Add a child element to the section's list column.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn add(mut self, item: impl Into<Element<'a, Message>>) -> Self {
|
||||
self.children = self.children.add(item.into());
|
||||
pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self {
|
||||
self.children = self.children.add(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.add(item)
|
||||
} else {
|
||||
|
|
@ -55,13 +63,13 @@ impl<'a, Message: 'static> Section<'a, Message> {
|
|||
/// Extends the [`Section`] with the given children.
|
||||
pub fn extend(
|
||||
self,
|
||||
children: impl IntoIterator<Item = impl Into<Element<'a, Message>>>,
|
||||
children: impl IntoIterator<Item = impl IntoListItem<'a, Message>>,
|
||||
) -> Self {
|
||||
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 {
|
||||
column::with_capacity(2)
|
||||
.spacing(8)
|
||||
|
|
|
|||
|
|
@ -161,7 +161,10 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
|
|||
}
|
||||
|
||||
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> {
|
||||
|
|
@ -238,6 +241,14 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
|
|||
return;
|
||||
};
|
||||
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 {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
|
|
@ -246,6 +257,7 @@ impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a,
|
|||
if mouse_over {
|
||||
shell.publish((on_toggle)(!self.is_toggled));
|
||||
state.anim.changed(self.duration);
|
||||
state.prev_toggled = !self.is_toggled;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
|
|
@ -430,4 +442,5 @@ pub fn next_to_each_other(
|
|||
pub struct State {
|
||||
text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
|
||||
anim: anim::State,
|
||||
prev_toggled: bool,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue