diff --git a/CHANGELOG.md b/CHANGELOG.md index 6093f487f..1675e42f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he - **core**: Added `OverrideClass` to override a single class within a subtree. (#657 @M-Adoo) - **widgets**: Added `LinearProgress` and `SpinnerProgress` widgets along with their respective material themes. (#630 @wjian23 @M-Adoo) +### Changed + +- **widgets**: The `Icon` widget utilizes classes to configure its style, and it does not have a size property. (#pr @M-Adoo) + ### Fixed - **core**: The size of the `Root` container is too small, which could lead to potential missed hits. (#654 @M-Adoo) diff --git a/core/src/builtin_widgets/theme.rs b/core/src/builtin_widgets/theme.rs index b254a082b..e9c8c7d28 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -85,8 +85,19 @@ pub struct Theme { pub transitions_theme: TransitionTheme, pub compose_decorators: ComposeDecorators, pub custom_styles: CustomStyles, - pub font_bytes: Option>>, - pub font_files: Option>, + pub font_bytes: Vec>, + pub font_files: Vec, + /// This font serves as the default choice for icons and is applied by the + /// standard `ICON` class. It is important to ensure that this font is + /// included in either `font_bytes` or `font_files`. + /// + /// Theme makers may not know which icons the application will utilize, making + /// it challenging to provide a default icon font. Additionally, offering a + /// vast selection of icons in a single font file can result in a large file + /// size, which is not ideal for web platforms. Therefore, this configuration + /// allows the application developer to supply the font file. This approach + /// supports the use of `SVG` and [`named_svgs`](super::named_svgs). + pub default_icon_font: FontFace, } impl Theme { @@ -107,16 +118,13 @@ impl Theme { fn load_fonts(&mut self) { let mut font_db = AppCtx::font_db().borrow_mut(); let Theme { font_bytes, font_files, .. } = self; - if let Some(font_bytes) = font_bytes { - font_bytes - .iter() - .for_each(|data| font_db.load_from_bytes(data.clone())); - } - if let Some(font_files) = font_files { - font_files.iter().for_each(|path| { - let _ = font_db.load_font_file(path); - }); - } + font_bytes + .iter() + .for_each(|data| font_db.load_from_bytes(data.clone())); + + font_files.iter().for_each(|path| { + let _ = font_db.load_font_file(path); + }); } } @@ -232,13 +240,14 @@ impl Default for Theme { Theme { palette: Palette::default(), typography_theme: typography_theme(), - classes: <_>::default(), icon_theme, + classes: <_>::default(), transitions_theme: Default::default(), compose_decorators: Default::default(), custom_styles: Default::default(), - font_bytes: None, - font_files: None, + font_bytes: vec![], + font_files: vec![], + default_icon_font: Default::default(), } } } diff --git a/core/src/test_helper.rs b/core/src/test_helper.rs index 03cc75e17..24ecd3c7b 100644 --- a/core/src/test_helper.rs +++ b/core/src/test_helper.rs @@ -355,15 +355,21 @@ impl LayoutCase { pub struct WidgetTester { pub widget: GenWidget, pub wnd_size: Option, + pub env_init: Option>, pub on_initd: Option, pub comparison: Option, } -type InitdFn = Box; +type InitdFn = Box; impl WidgetTester { pub fn new(widget: impl Into) -> Self { - Self { wnd_size: None, widget: widget.into(), on_initd: None, comparison: None } + Self { wnd_size: None, widget: widget.into(), on_initd: None, env_init: None, comparison: None } + } + + pub fn with_env_init(mut self, env_init: impl Fn() + 'static) -> Self { + self.env_init = Some(Box::new(env_init)); + self } /// This callback runs after creating the window and drawing the first frame. @@ -382,11 +388,15 @@ impl WidgetTester { self } - pub fn create_wnd(&self) -> TestWindow { + pub fn create_wnd(&mut self) -> TestWindow { + if let Some(env_init) = self.env_init.take() { + env_init(); + } + let wnd_size = self.wnd_size.unwrap_or(Size::new(1024., 1024.)); let mut wnd = TestWindow::new_with_size(self.widget.clone(), wnd_size); wnd.draw_frame(); - if let Some(initd) = self.on_initd.as_ref() { + if let Some(initd) = self.on_initd.take() { initd(&mut wnd); wnd.draw_frame(); } @@ -394,7 +404,7 @@ impl WidgetTester { } #[track_caller] - pub fn layout_check(&self, cases: &[LayoutCase]) { + pub fn layout_check(mut self, cases: &[LayoutCase]) { let wnd = self.create_wnd(); cases.iter().for_each(|c| c.check(&wnd)); } diff --git a/core/src/widget_children/child_convert.rs b/core/src/widget_children/child_convert.rs index df926ebff..c43dabe66 100644 --- a/core/src/widget_children/child_convert.rs +++ b/core/src/widget_children/child_convert.rs @@ -1,3 +1,4 @@ +use super::{DeclareInit, DeclareInto}; use crate::{pipe::*, widget::*}; /// Trait for conversions type as a child of widget. @@ -94,3 +95,19 @@ where #[inline] fn into_child(self) -> FnWidget<'w> { FnWidget::new(self) } } + +impl IntoChild, RENDER> for T +where + T: DeclareInto, +{ + #[inline] + fn into_child(self) -> DeclareInit { self.declare_into() } +} + +impl IntoChild, COMPOSE> for T +where + T: DeclareInto, +{ + #[inline] + fn into_child(self) -> DeclareInit { self.declare_into() } +} diff --git a/dev-helper/src/widget_test.rs b/dev-helper/src/widget_test.rs index 77f9ed992..af0aea20f 100644 --- a/dev-helper/src/widget_test.rs +++ b/dev-helper/src/widget_test.rs @@ -50,9 +50,14 @@ macro_rules! widget_layout_test { #[macro_export] macro_rules! widget_image_tests { ($name:ident, $widget_tester:expr) => { - widget_image_tests!(gen_test: $name, with_default_by_wgpu, Theme::default(), $widget_tester); - widget_image_tests!(gen_test: - $name, + widget_image_tests!( + gen_test: $name, + with_default_by_wgpu, + ribir_slim::purple(), + $widget_tester + ); + widget_image_tests!( + gen_test: $name, with_material_by_wgpu, ribir_material::purple::light(), $widget_tester diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 53c954aa8..b6c4bc7de 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -14,7 +14,7 @@ version.workspace = true [dependencies] paste.workspace = true # we disable `default-features`, because we want more control over testing. -ribir = {path = "../../ribir", features = ["material", "widgets"]} +ribir = {path = "../../ribir", features = ["material", "slim", "widgets"]} [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" diff --git a/examples/messages/Cargo.toml b/examples/messages/Cargo.toml index 8dec37aa5..f2ee0fffd 100644 --- a/examples/messages/Cargo.toml +++ b/examples/messages/Cargo.toml @@ -14,7 +14,7 @@ version.workspace = true [dependencies] paste.workspace = true # we disable `default-features`, because we want more control over testing. -ribir = {path = "../../ribir", features = ["material", "widgets"]} +ribir = {path = "../../ribir", features = ["material", "slim", "widgets"]} [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" diff --git a/examples/messages/src/messages.rs b/examples/messages/src/messages.rs index ed043bf55..5386503a4 100644 --- a/examples/messages/src/messages.rs +++ b/examples/messages/src/messages.rs @@ -53,7 +53,7 @@ impl Compose for MessageList { align_items: Align::Center, @Row { item_gap: 10., - @TinyIcon { @{ svgs::MENU } } + @Icon { @{ svgs::MENU } } @Text { text: "Message", foreground: palette.on_surface(), @@ -62,8 +62,8 @@ impl Compose for MessageList { } @Row { item_gap: 10., - @TinyIcon { @{ svgs::SEARCH } } - @TinyIcon { @{ svgs::MORE_VERT } } + @Icon { @{ svgs::SEARCH } } + @Icon { @{ svgs::MORE_VERT } } } } @Tabs { @@ -116,7 +116,7 @@ impl Compose for MessageList { #[cfg(test)] mod tests { - use ribir::{core::test_helper::*, material as ribir_material}; + use ribir::{core::test_helper::*, material as ribir_material, slim as ribir_slim}; use ribir_dev_helper::*; use super::*; diff --git a/examples/storybook/src/lib.rs b/examples/storybook/src/lib.rs index 0615d0b4a..4aadeae6e 100644 --- a/examples/storybook/src/lib.rs +++ b/examples/storybook/src/lib.rs @@ -15,7 +15,7 @@ pub fn run() { #[cfg(test)] mod tests { - use ribir::{core::test_helper::*, material as ribir_material}; + use ribir::{core::test_helper::*, material as ribir_material, slim as ribir_slim}; use ribir_dev_helper::*; use super::*; diff --git a/examples/storybook/src/storybook.rs b/examples/storybook/src/storybook.rs index 39b4de248..3458407c5 100644 --- a/examples/storybook/src/storybook.rs +++ b/examples/storybook/src/storybook.rs @@ -26,7 +26,6 @@ fn content() -> Widget<'static> { clamp: BoxClamp::fixed_height(30.), @Text { text: "Common buttons" } @Icon { - size: Size::splat(16.), @ { material_svgs::INFO } } } @@ -96,7 +95,6 @@ fn content() -> Widget<'static> { @Row { @Text { text: "Floating action buttons" } @Icon { - size: Size::splat(16.), @ { material_svgs::INFO } } } @@ -129,7 +127,6 @@ fn content() -> Widget<'static> { @Row { @Text { text: "Icon buttons" } @Icon { - size: Size::splat(16.), @ { material_svgs::INFO } } } @@ -201,7 +198,6 @@ fn content() -> Widget<'static> { h_align: HAlign::Center, @Text { text: "Divider" } @Icon { - size: Size::splat(16.), @ { material_svgs::INFO } } } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 06a422003..ebd27f91d 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -14,7 +14,7 @@ version.workspace = true [dependencies] paste.workspace = true # we disable `default-features`, because we want more control over testing. -ribir = {path = "../../ribir", features = ["material", "widgets"]} +ribir = {path = "../../ribir", features = ["material", "slim", "widgets"]} serde = {workspace = true, features = ["derive"]} serde_json = {workspace = true} diff --git a/examples/todos/src/lib.rs b/examples/todos/src/lib.rs index 876c2bc12..441ae1924 100644 --- a/examples/todos/src/lib.rs +++ b/examples/todos/src/lib.rs @@ -15,7 +15,7 @@ pub fn run() { #[cfg(test)] mod tests { - use ribir::{core::test_helper::*, material as ribir_material}; + use ribir::{core::test_helper::*, material as ribir_material, slim as ribir_slim}; use ribir_dev_helper::*; use super::*; diff --git a/examples/wordle_game/Cargo.toml b/examples/wordle_game/Cargo.toml index 67b07cf36..346da2508 100644 --- a/examples/wordle_game/Cargo.toml +++ b/examples/wordle_game/Cargo.toml @@ -14,7 +14,7 @@ version.workspace = true [dependencies] paste.workspace = true -ribir = {path = "../../ribir", features = ["material", "widgets"]} +ribir = {path = "../../ribir", features = ["material", "slim", "widgets"]} csv = "1.3.0" rand = "0.8.5" getrandom = { workspace = true } diff --git a/examples/wordle_game/src/lib.rs b/examples/wordle_game/src/lib.rs index 0dcaa0d15..1c8287b8a 100644 --- a/examples/wordle_game/src/lib.rs +++ b/examples/wordle_game/src/lib.rs @@ -15,7 +15,7 @@ pub fn run() { #[cfg(test)] mod tests { - use ribir::{core::test_helper::*, material as ribir_material}; + use ribir::{core::test_helper::*, material as ribir_material, slim as ribir_slim}; use ribir_dev_helper::*; use super::*; diff --git a/fonts/material-search.ttf b/fonts/material-search.ttf new file mode 100644 index 000000000..56377dc36 Binary files /dev/null and b/fonts/material-search.ttf differ diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 90ffaed5d..f8045892f 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -156,12 +156,12 @@ pub fn pipe(input: TokenStream) -> TokenStream { pub fn style_class(input: TokenStream) -> TokenStream { let input: proc_macro2::TokenStream = input.into(); quote! { - move |widget: Widget| { + (move |widget: Widget| { fn_widget! { let widget = FatObj::new(widget); @ $widget { #input } }.into_widget() - } + }) as fn(Widget) -> Widget } .into() } diff --git a/painter/src/text/font_db.rs b/painter/src/text/font_db.rs index 6e173e1f1..0b36207c9 100644 --- a/painter/src/text/font_db.rs +++ b/painter/src/text/font_db.rs @@ -120,17 +120,13 @@ impl FontDB { families .iter() .filter_map(|f| { - if let Some(id) = self.data_base.query(&Query { + let id = self.data_base.query(&Query { families: &[to_db_family(f)], weight: *weight, stretch: *stretch, style: *style, - }) { - if self.face_data_or_insert(id).is_some() { - return Some(id); - } - } - None + })?; + self.face_data_or_insert(id).map(|_| id) }) .collect() } diff --git a/ribir/Cargo.toml b/ribir/Cargo.toml index 37431d810..c1966e7a8 100644 --- a/ribir/Cargo.toml +++ b/ribir/Cargo.toml @@ -17,6 +17,7 @@ ribir_algo = { path = "../algo", version = "0.4.0-alpha.15" } ribir_core = { path = "../core", version = "0.4.0-alpha.15" } ribir_gpu = { path = "../gpu", version = "0.4.0-alpha.15" } ribir_material = { path = "../themes/material", version = "0.4.0-alpha.15", optional = true } +ribir_slim = { path = "../themes/ribir_slim", version = "0.4.0-alpha.15", optional = true } ribir_widgets = { path = "../widgets", version = "0.4.0-alpha.15", optional = true } rxrust.workspace = true wgpu = { workspace = true, optional = true } @@ -54,6 +55,7 @@ wgpu = ["ribir_gpu/wgpu", "dep:wgpu"] widgets = ["ribir_widgets"] tokio-async = ["ribir_core/tokio-async"] nightly = ["ribir_core/nightly"] +slim = ["ribir_slim"] [[test]] harness = false diff --git a/ribir/src/lib.rs b/ribir/src/lib.rs index 79a5d15ff..34477daed 100644 --- a/ribir/src/lib.rs +++ b/ribir/src/lib.rs @@ -9,6 +9,8 @@ pub mod clipboard; mod winit_shell_wnd; #[cfg(feature = "material")] pub use ribir_material as material; +#[cfg(feature = "slim")] +pub use ribir_slim as slim; mod platform; pub use platform::*; @@ -17,6 +19,8 @@ pub mod prelude { #[cfg(feature = "material")] pub use super::material; + #[cfg(feature = "slim")] + pub use super::slim; #[cfg(feature = "widgets")] pub use super::widgets::prelude::*; pub use crate::app::*; diff --git a/test_cases/messages/messages/tests/messages_with_default_by_wgpu.png b/test_cases/messages/messages/tests/messages_with_default_by_wgpu.png index ffab7e95e..dd34c5135 100644 Binary files a/test_cases/messages/messages/tests/messages_with_default_by_wgpu.png and b/test_cases/messages/messages/tests/messages_with_default_by_wgpu.png differ diff --git a/test_cases/messages/messages/tests/messages_with_material_by_wgpu.png b/test_cases/messages/messages/tests/messages_with_material_by_wgpu.png index 5d44c5de0..7e8d25339 100644 Binary files a/test_cases/messages/messages/tests/messages_with_material_by_wgpu.png and b/test_cases/messages/messages/tests/messages_with_material_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png index a7ff45723..40643243c 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png index a7ff45723..927a1ebac 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png index a7ff45723..a1ec87942 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png b/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png new file mode 100644 index 000000000..f4d7ac8e4 Binary files /dev/null and b/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png b/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png new file mode 100644 index 000000000..e5945be9f Binary files /dev/null and b/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png differ diff --git a/test_cases/storybook/tests/storybook_with_default_by_wgpu.png b/test_cases/storybook/tests/storybook_with_default_by_wgpu.png index 0a25314ca..e724b42e0 100644 Binary files a/test_cases/storybook/tests/storybook_with_default_by_wgpu.png and b/test_cases/storybook/tests/storybook_with_default_by_wgpu.png differ diff --git a/test_cases/storybook/tests/storybook_with_material_by_wgpu.png b/test_cases/storybook/tests/storybook_with_material_by_wgpu.png index 6ad078c93..263085638 100644 Binary files a/test_cases/storybook/tests/storybook_with_material_by_wgpu.png and b/test_cases/storybook/tests/storybook_with_material_by_wgpu.png differ diff --git a/themes/material/src/classes.rs b/themes/material/src/classes.rs index 504c237c6..de9e0d532 100644 --- a/themes/material/src/classes.rs +++ b/themes/material/src/classes.rs @@ -1,10 +1,14 @@ use ribir_core::prelude::Classes; +mod icon_cls; mod progress_cls; mod radio_cls; mod scrollbar_cls; + pub fn initd_classes() -> Classes { let mut classes = Classes::default(); + + icon_cls::init(&mut classes); scrollbar_cls::init(&mut classes); radio_cls::init(&mut classes); progress_cls::init(&mut classes); diff --git a/themes/material/src/classes/icon_cls.rs b/themes/material/src/classes/icon_cls.rs new file mode 100644 index 000000000..4c0e4b4b0 --- /dev/null +++ b/themes/material/src/classes/icon_cls.rs @@ -0,0 +1,23 @@ +use ribir_core::prelude::*; +use ribir_widgets::icon::ICON; + +use crate::md; + +pub(super) fn init(classes: &mut Classes) { + classes.insert(ICON, |w| { + let font_face = Theme::of(BuildCtx::get()) + .default_icon_font + .clone(); + + FatObj::new(w) + .clamp(BoxClamp::fixed_size(md::SIZE_24)) + .text_style(TextStyle { + line_height: 24., + font_size: 24., + letter_space: 0., + font_face, + overflow: Overflow::Clip, + }) + .into_widget() + }); +} diff --git a/themes/material/src/lib.rs b/themes/material/src/lib.rs index 691ee6252..a4e7fc55b 100644 --- a/themes/material/src/lib.rs +++ b/themes/material/src/lib.rs @@ -17,14 +17,11 @@ fn new(palette: Palette) -> Theme { typography_theme: typography_theme(), classes, icon_theme: icon_theme(), - transitions_theme: TransitionTheme::default(), - compose_decorators: <_>::default(), - custom_styles: <_>::default(), - font_bytes: Some(vec![ + font_bytes: vec![ include_bytes!("./fonts/Roboto-Regular.ttf").to_vec(), include_bytes!("./fonts/Roboto-Medium.ttf").to_vec(), - ]), - font_files: None, + ], + ..Default::default() }; fill_svgs! { theme.icon_theme, diff --git a/themes/ribir_slim/src/classes.rs b/themes/ribir_slim/src/classes.rs new file mode 100644 index 000000000..9e5094140 --- /dev/null +++ b/themes/ribir_slim/src/classes.rs @@ -0,0 +1,10 @@ +use ribir_core::prelude::Classes; + +mod icon_cls; + +pub fn initd_classes() -> Classes { + let mut classes = Classes::default(); + + icon_cls::init(&mut classes); + classes +} diff --git a/themes/ribir_slim/src/classes/icon_cls.rs b/themes/ribir_slim/src/classes/icon_cls.rs new file mode 100644 index 000000000..9af32dc09 --- /dev/null +++ b/themes/ribir_slim/src/classes/icon_cls.rs @@ -0,0 +1,23 @@ +use ribir_core::prelude::*; +use ribir_widgets::icon::ICON; + +use crate::slim; + +pub(super) fn init(classes: &mut Classes) { + classes.insert(ICON, |w| { + let font_face = Theme::of(BuildCtx::get()) + .default_icon_font + .clone(); + + FatObj::new(w) + .clamp(BoxClamp::fixed_size(slim::SIZE_24)) + .text_style(TextStyle { + line_height: 24., + font_size: 24., + letter_space: 0., + font_face, + overflow: Overflow::Clip, + }) + .into_widget() + }); +} diff --git a/themes/ribir_slim/src/lib.rs b/themes/ribir_slim/src/lib.rs index 97a1d0530..a42a67ad3 100644 --- a/themes/ribir_slim/src/lib.rs +++ b/themes/ribir_slim/src/lib.rs @@ -1,6 +1,27 @@ use ribir_core::{fill_svgs, prelude::*}; +mod classes; +pub mod slim; + +pub fn purple() -> Theme { + let p = Palette { + primary: Color::from_u32(0x6750A4FF), + secondary: Color::from_u32(0x625B71FF), + tertiary: Color::from_u32(0x7D5260FF), + neutral: Color::from_u32(0xFFFBFEFF), + neutral_variant: Color::from_u32(0xE7E0ECFF), + error: Color::from_u32(0xB3261EFF), + warning: Color::from_u32(0xFFB74DFF), + success: Color::from_u32(0x81C784FF), + brightness: Brightness::Light, + light: LightnessCfg::light_theme_default(), + dark: LightnessCfg::dark_theme_default(), + }; + + with_palette(p) +} pub fn with_palette(palette: Palette) -> Theme { + let classes = classes::initd_classes(); let icon_size = IconSize { tiny: Size::new(18., 18.), small: Size::new(24., 24.), @@ -39,17 +60,7 @@ pub fn with_palette(palette: Palette) -> Theme { svgs::TEXT_CARET: "./icons/text_caret.svg" }; - Theme { - palette, - typography_theme: typography_theme(), - classes: <_>::default(), - icon_theme, - transitions_theme: Default::default(), - compose_decorators: Default::default(), - custom_styles: Default::default(), - font_bytes: None, - font_files: None, - } + Theme { palette, classes, typography_theme: typography_theme(), icon_theme, ..Default::default() } } fn typography_theme() -> TypographyTheme { diff --git a/themes/ribir_slim/src/slim.rs b/themes/ribir_slim/src/slim.rs new file mode 100644 index 000000000..85593da1a --- /dev/null +++ b/themes/ribir_slim/src/slim.rs @@ -0,0 +1,7 @@ +use ribir_core::prelude::*; + +pub const SIZE_16: Size = Size::new(16.0, 16.0); +pub const SIZE_24: Size = Size::new(24.0, 24.0); +pub const SIZE_32: Size = Size::new(32.0, 32.0); +pub const SIZE_48: Size = Size::new(48.0, 48.0); +pub const SIZE_64: Size = Size::new(64.0, 64.0); diff --git a/widgets/Cargo.toml b/widgets/Cargo.toml index f8c675296..9fff6f55f 100644 --- a/widgets/Cargo.toml +++ b/widgets/Cargo.toml @@ -26,3 +26,4 @@ paste.workspace = true winit.workspace = true ribir_dev_helper = {path = "../dev-helper"} ribir_material = {path = "../themes/material"} +ribir_slim = {path = "../themes/ribir_slim"} diff --git a/widgets/src/buttons.rs b/widgets/src/buttons.rs index 7c76532a6..b1990c16b 100644 --- a/widgets/src/buttons.rs +++ b/widgets/src/buttons.rs @@ -59,7 +59,6 @@ impl ComposeChild<'static> for ButtonImpl { @{ let padding = $this.padding_style.map(Padding::new); let icon = icon.map(|icon| @Icon { - size: pipe!($this.icon_size), @{ icon } }); let label = label.map(|label| @Text { diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index 6c77fe64d..38d8c62c7 100644 --- a/widgets/src/checkbox.rs +++ b/widgets/src/checkbox.rs @@ -2,6 +2,7 @@ use ribir_core::prelude::*; use crate::{ common_widget::{Leading, Trailing}, + icon::ICON, prelude::{Icon, Label, Row, Text}, }; @@ -59,55 +60,62 @@ impl ComposeChild<'static> for Checkbox { fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'static> { fn_widget! { let CheckBoxStyle { - icon_size, label_style, label_color, + .. } = CheckBoxStyle::of(BuildCtx::get()); - - let icon = @CheckBoxDecorator { - color: pipe!($this.color), - @Icon { size: icon_size, - @ { pipe!{ - if $this.indeterminate { - svgs::INDETERMINATE_CHECK_BOX - } else if $this.checked { - svgs::CHECK_BOX - } else { - svgs::CHECK_BOX_OUTLINE_BLANK + @OverrideClass { + name: ICON, + class_impl: style_class! { + clamp: BoxClamp::fixed_size(CheckBoxStyle::of(BuildCtx::get()).icon_size) + }, + @fn_widget! { + let icon = @CheckBoxDecorator { + color: pipe!($this.color), + @Icon { + @ { pipe!{ + if $this.indeterminate { + svgs::INDETERMINATE_CHECK_BOX + } else if $this.checked { + svgs::CHECK_BOX + } else { + svgs::CHECK_BOX_OUTLINE_BLANK + } + }} } - }} - } - }.into_widget(); - - let checkbox = if let Some(child) = child { - let label = |label: Label| @Text { - text: label.0, - foreground: label_color, - text_style: label_style, - }; - - @Row { - @ { - match child { - CheckboxTemplate::Before(w) => { - [(label(w.0)).into_widget(), icon] - }, - CheckboxTemplate::After(w) => { - [icon, label(w.0).into_widget()] - }, + }.into_widget(); + + let checkbox = if let Some(child) = child { + let label = |label: Label| @Text { + text: label.0, + foreground: label_color, + text_style: label_style, + }; + + @Row { + @ { + match child { + CheckboxTemplate::Before(w) => { + [(label(w.0)).into_widget(), icon] + }, + CheckboxTemplate::After(w) => { + [icon, label(w.0).into_widget()] + }, + } + } + }.into_widget() + } else { + icon + }; + + let checkbox = FatObj::new(checkbox); + @ $checkbox { + cursor: CursorIcon::Pointer, + on_tap: move |_| $this.write().switch_check(), + on_key_up: move |k| if *k.key() == VirtualKey::Named(NamedKey::Space) { + $this.write().switch_check() } } - }.into_widget() - } else { - icon - }; - - let checkbox = FatObj::new(checkbox); - @ $checkbox { - cursor: CursorIcon::Pointer, - on_tap: move |_| $this.write().switch_check(), - on_key_up: move |k| if *k.key() == VirtualKey::Named(NamedKey::Space) { - $this.write().switch_check() } } } diff --git a/widgets/src/icon.rs b/widgets/src/icon.rs index ea997eaec..b76c57f93 100644 --- a/widgets/src/icon.rs +++ b/widgets/src/icon.rs @@ -1,60 +1,135 @@ use ribir_core::prelude::*; -use crate::layout::SizedBox; +use crate::text::*; -/// Widget that let child paint as a icon with special size. +/// An icon widget represents an icon. /// -/// Unlike icon in classic frameworks, it's not draw anything and not require -/// you to provide image or font fot it to draw, it just center align and fit -/// size of its child. So you can declare any widget as its child to display as -/// a icon. +/// It can accept either text or another widget as its child. If the child is +/// text, the widget uses the font ligature to display the text as an icon. +/// Therefore, an icon font must be provided. +/// +/// +/// # Example +/// +/// ``` +/// use ribir_core::prelude::*; +/// use ribir_widgets::prelude::*; +/// +/// // To use an icon font, set the icon font before running the app. +/// let mut theme = AppCtx::app_theme().write(); +/// theme +/// .font_files +/// .push("the font file path".to_string()); +/// theme.default_icon_font = FontFace { +/// families: Box::new([FontFamily::Name("Your icon font family name".into())]), +/// // The rest of the face configuration depends on your font file +/// ..<_>::default() +/// }; +/// +/// // Using a named SVG as an icon +/// let _icon = icon! { @ { svgs::DELETE } }; +/// // Using a font icon +/// let _icon = icon! { @ { "search" } }; +/// // Using any widget you want +/// let _icon = icon! { +/// @Container { +/// size: Size::new(200., 200.), +/// background: Color::RED, +/// } +/// }; +/// ``` +/// +/// The size of the icon is determined by the `ICON` class. If you need a +/// different size for the icon, you can override the `ICON` class to apply the +/// changes to the icon within its subtree. +/// +/// +/// ``` +/// use ribir_core::prelude::*; +/// use ribir_widgets::prelude::*; +/// +/// let w = fn_widget! { +/// @OverrideClass { +/// name: ICON, +/// class_impl: style_class! { +/// clamp: BoxClamp::fixed_size(Size::new(64., 64.)), +/// text_style: TextStyle { +/// font_face: Theme::of(BuildCtx::get()).default_icon_font.clone(), +/// line_height: 64., +/// font_size: 64., +/// ..<_>::default() +/// } +/// } as ClassImpl, +/// @icon! { @ { svgs::DELETE } } +/// } +/// }; +/// ``` #[derive(Declare, Default, Clone, Copy)] -pub struct Icon { - #[declare(default = IconSize::of(BuildCtx::get()).small)] - pub size: Size, +pub struct Icon; + +class_names! { + #[doc = "This class is used to specify the size of the icon and the text style for the icon."] + ICON, +} + +#[derive(Template)] +pub enum IconChild<'c> { + /// The text to display as a icon. + /// + /// Use a `DeclareInit>` so that we can accept a pipe text. + FontIcon(DeclareInit>), + Widget(Widget<'c>), } impl<'c> ComposeChild<'c> for Icon { - type Child = Widget<'c>; - fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { - fn_widget! { - let child = FatObj::new(child); - @SizedBox { - size: pipe!($this.size), - @ $child { - box_fit: BoxFit::Contain, - h_align: HAlign::Center, - v_align: VAlign::Center, - } - } - } - .into_widget() + type Child = IconChild<'c>; + fn compose_child(_: impl StateWriter, child: Self::Child) -> Widget<'c> { + let child = match child { + IconChild::FontIcon(text) => text! { text }.into_widget(), + IconChild::Widget(child) => child, + }; + + let icon = FatObj::new(child) + .box_fit(BoxFit::Contain) + .h_align(HAlign::Center) + .v_align(VAlign::Center) + .into_widget(); + + // We need apply class after align and box_fit. + Class { class: Some(ICON) } + .with_child(icon) + .into_widget() } } -macro_rules! define_fixed_size_icon { - ($($name: ident, $field: ident),*) => { - $( - #[derive(Declare, Default, Clone, Copy)] - pub struct $name; - - impl<'c> ComposeChild<'c> for $name { - type Child = Widget<'c>; - fn compose_child(_: impl StateWriter, child: Self::Child) - -> Widget<'c> - { - fn_widget! { - let icon = @Icon { size: IconSize::of(BuildCtx::get()).$field }; - @ $icon { @ { child } } - }.into_widget() - } - } - )* - }; -} +#[cfg(test)] +mod tests { + use ribir_core::test_helper::*; + use ribir_dev_helper::*; -define_fixed_size_icon!(TinyIcon, tiny); -define_fixed_size_icon!(SmallIcon, small); -define_fixed_size_icon!(MediumIcon, medium); -define_fixed_size_icon!(LargeIcon, large); -define_fixed_size_icon!(HugeIcon, huge); + use super::*; + use crate::prelude::*; + + widget_image_tests!( + icons, + WidgetTester::new(row! { + @Icon { @ { svgs::DELETE }} + @Icon { @ { "search" } } + @Icon { @SpinnerProgress { value: Some(0.8) }} + }) + .with_wnd_size(Size::new(300., 200.)) + .with_env_init(|| { + let mut theme = AppCtx::app_theme().write(); + // Specify the icon font. + theme + .font_bytes + .push(include_bytes!("../../fonts/material-search.ttf").to_vec()); + theme.default_icon_font = FontFace { + families: Box::new([FontFamily::Name("Material Symbols Rounded 48pt".into())]), + weight: FontWeight::NORMAL, + ..<_>::default() + }; + }) + .with_comparison(0.002) + ); +} diff --git a/widgets/src/lists.rs b/widgets/src/lists.rs index 12c2cf8cb..72e1156ee 100644 --- a/widgets/src/lists.rs +++ b/widgets/src/lists.rs @@ -196,10 +196,7 @@ impl<'w> EdgeWidget<'w> { EdgeWidget::Icon(w) => { let margin = icon.gap.map(|margin| Margin { margin }); @ $margin { - @Icon { - size: icon.size, - @ { w } - } + @Icon { @ { w } } } }, EdgeWidget::Text(label) => { diff --git a/widgets/src/progress.rs b/widgets/src/progress.rs index bc5383dc2..15455f5f0 100644 --- a/widgets/src/progress.rs +++ b/widgets/src/progress.rs @@ -115,17 +115,20 @@ pub struct SpinnerArc { impl Render for SpinnerArc { fn only_sized_by_parent(&self) -> bool { true } - fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size { clamp.max } + fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { + let wnd_size = ctx.window().size(); + clamp.max.min(wnd_size) + } fn paint(&self, ctx: &mut PaintingCtx) { let Self { start, end } = *self; - if self.offset_angle().to_degrees().abs() < 0.1 { + let size = ctx.box_size().unwrap(); + if self.offset_angle().to_degrees().abs() < 0.1 || size.is_empty() { return; } let start = start - Angle::pi() / 2.; let end = end - Angle::pi() / 2.; - let size = ctx.box_size().unwrap(); let center = Point::new(size.width / 2., size.height / 2.); let radius = min(center.x, center.y); let painter = ctx.painter(); diff --git a/widgets/src/tabs.rs b/widgets/src/tabs.rs index 62996417d..f6f5addbd 100644 --- a/widgets/src/tabs.rs +++ b/widgets/src/tabs.rs @@ -160,8 +160,7 @@ impl Tabs { tabs: impl StateWriter + 'static, indicator: impl StateWriter + 'static, ) -> impl Iterator> { - let TabsStyle { icon_size: size, icon_pos, active_color, foreground, label_style, .. } = - tabs_style; + let TabsStyle { icon_pos, active_color, foreground, label_style, .. } = tabs_style; headers .into_iter() .enumerate() @@ -172,7 +171,7 @@ impl Tabs { let label_style = label_style.clone(); let indicator = indicator.clone_writer(); fn_widget! { - let icon_widget = icon.map(|icon| @Icon { size, @ { icon }}); + let icon_widget = icon.map(|icon| @Icon { @ { icon }}); let label_widget = label.map(|label| { @Text { text: label.0, diff --git a/widgets/src/text.rs b/widgets/src/text.rs index de315cc2b..5462cd2cb 100644 --- a/widgets/src/text.rs +++ b/widgets/src/text.rs @@ -54,6 +54,9 @@ impl Render for Text { } impl Text { + pub fn new(text: impl Into>) -> Self { + Self { text: text.into(), text_align: TextAlign::Start, glyphs: Default::default() } + } pub fn glyphs(&self) -> Option> { Ref::filter_map(self.glyphs.borrow(), |v| v.as_ref()).ok() } diff --git a/widgets/src/text_field.rs b/widgets/src/text_field.rs index d20f4c62a..c5b71b8da 100644 --- a/widgets/src/text_field.rs +++ b/widgets/src/text_field.rs @@ -288,7 +288,6 @@ impl<'c> ComposeChild<'c> for TextField { align_items: Align::Stretch, @{ leading_icon.map(|t| @Icon { - size: IconSize::of(ctx).small, @{ t.0 } }) } @@ -297,10 +296,7 @@ impl<'c> ComposeChild<'c> for TextField { @{ build_content_area(this, theme, config) } } @{ - trailing_icon.map(|t| @Icon { - size: IconSize::of(ctx).small, - @{ t.0 } - }) + trailing_icon.map(|t| @Icon { @{ t.0 } }) } } @Container {