diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ec5f876..599e3f60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ 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) - **painter**: SVG now supports switching the default color, allowing for icon color changes. (#661 @M-Adoo) +- **core**: Added the builtin widget of tooltips (#664 @wjian23) ### Changed diff --git a/core/Cargo.toml b/core/Cargo.toml index d306510d7..e0ba3205a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,7 @@ web-time.workspace = true colored.workspace = true paste.workspace = true ribir_dev_helper = {path = "../dev-helper"} -ribir = { path = "../ribir" } +ribir = { path = "../ribir", features = ["material", "slim"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["full"]} diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index d097c8fd1..fef11a257 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -75,6 +75,10 @@ pub use smooth_layout::*; mod track_widget_id; pub use track_widget_id::*; +mod text; +pub use text::*; +mod tooltips; +pub use tooltips::*; use crate::prelude::*; @@ -131,6 +135,7 @@ pub struct FatObj { painting_style: Option>, text_style: Option>, keep_alive: Option>, + tooltips: Option>, keep_alive_unsubscribe_handle: Option>, } @@ -165,6 +170,7 @@ impl FatObj { text_style: self.text_style, visibility: self.visibility, opacity: self.opacity, + tooltips: self.tooltips, keep_alive: self.keep_alive, keep_alive_unsubscribe_handle: self.keep_alive_unsubscribe_handle, } @@ -195,6 +201,7 @@ impl FatObj { && self.visibility.is_none() && self.opacity.is_none() && self.keep_alive.is_none() + && self.tooltips.is_none() } /// Return the host object of the FatObj. @@ -415,6 +422,14 @@ impl FatObj { .keep_alive .get_or_insert_with(|| State::value(<_>::default())) } + + /// Returns the `State` widget from the FatObj. If it doesn't + /// exist, a new one is created. + pub fn get_tooltips_widget(&mut self) -> &State { + self + .tooltips + .get_or_insert_with(|| State::value(<_>::default())) + } } macro_rules! on_mixin { @@ -827,6 +842,11 @@ impl FatObj { self.declare_builtin_init(v, Self::get_opacity_widget, |m, v| m.opacity = v) } + /// Initializes the tooltips of the widget. + pub fn tooltips(self, v: impl DeclareInto, M>) -> Self { + self.declare_builtin_init(v, Self::get_tooltips_widget, |m, v| m.tooltips = v) + } + /// Initializes the `keep_alive` value of the `KeepAlive` widget. pub fn keep_alive(mut self, v: impl DeclareInto) -> Self { let (v, o) = v.declare_into().unzip(); @@ -914,6 +934,7 @@ impl<'a> FatObj> { class, cursor, constrained_box, + tooltips, margin, transform, opacity, diff --git a/widgets/src/text.rs b/core/src/builtin_widgets/text.rs similarity index 94% rename from widgets/src/text.rs rename to core/src/builtin_widgets/text.rs index 2a1dafe91..deb200073 100644 --- a/widgets/src/text.rs +++ b/core/src/builtin_widgets/text.rs @@ -1,8 +1,9 @@ use std::cell::{Ref, RefCell}; -use ribir_core::prelude::*; use typography::PlaceLineDirection; +use crate::prelude::*; + pub type TextInit = DeclareInit>; /// The text widget display text with a single style. #[derive(Declare)] @@ -91,19 +92,17 @@ define_text_with_theme_style!(H4, title_large); define_text_with_theme_style!(H5, title_medium); define_text_with_theme_style!(H6, title_small); -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use ribir_core::test_helper::*; + use ribir::{core::test_helper::*, material as ribir_material, prelude::*, slim as ribir_slim}; use ribir_dev_helper::*; - use super::*; - use crate::layout::SizedBox; const WND_SIZE: Size = Size::new(164., 64.); widget_test_suit!( text_clip, WidgetTester::new(fn_widget! { - @SizedBox { + @ MockBox { size: Size::new(50., 45.), @Text { text: "hello world,\rnice to meet you.", diff --git a/core/src/builtin_widgets/tooltips.rs b/core/src/builtin_widgets/tooltips.rs new file mode 100644 index 000000000..4d5e87273 --- /dev/null +++ b/core/src/builtin_widgets/tooltips.rs @@ -0,0 +1,80 @@ +use crate::prelude::*; + +class_names! { + #[doc = "Class name for the tooltips"] + TOOLTIPS, +} + +/// Add attributes of tooltips to Widget Declarer. +/// +/// ### Example: +/// ```no_run +/// use ribir::prelude::*; +/// +/// let w = fn_widget! { +/// @FilledButton{ +/// text: "hover to show tooltips!", +/// tooltips: "this is tooltips", +/// } +/// }; +/// App::run(w); +/// ``` +#[derive(Default)] +pub struct Tooltips { + pub tooltips: CowArc, +} + +impl Declare for Tooltips { + type Builder = FatObj<()>; + fn declarer() -> Self::Builder { FatObj::new(()) } +} + +impl Tooltips { + fn tooltips(&self) -> &CowArc { &self.tooltips } +} + +impl<'c> ComposeChild<'c> for Tooltips { + type Child = Widget<'c>; + fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { + fn_widget! { + let wnd = BuildCtx::get().window(); + let mut child = FatObj::new(child); + + watch!($child.is_hover()) + .distinct_until_changed() + .filter(|v| *v) + .subscribe(move |_| { + let this = this.clone_writer(); + let overlay = Overlay::new( + text! { + text: pipe!($this.tooltips().clone()), + class: TOOLTIPS, + }, OverlayStyle { + auto_close_policy: AutoClosePolicy::NOT_AUTO_CLOSE, + mask: None, + }); + + overlay.clone().show_map(move |w: Widget| { + let overlay = overlay.clone(); + watch!($child.is_hover()) + .distinct_until_changed() + .filter(|v| !v) + .take(1) + .subscribe(move |_| overlay.close()); + let mut w = FatObj::new(w); + let anchor_widget = w.get_global_anchor_widget().clone_writer(); + @ $w { + anchor: pipe!(Anchor::left(-$w.layout_size().width / 2.)), + on_mounted: move|e| { + let wid = $child.track_id(); + anchor_widget.bottom_align_to(wid.clone(), $child.layout_size().height, e.window()); + anchor_widget.left_align_to(wid, $child.layout_size().width / 2., e.window()); + } + }.into_widget() + }, wnd.clone()); + }); + @ { child } + } + .into_widget() + } +} diff --git a/macros/src/declare_derive.rs b/macros/src/declare_derive.rs index 169979073..76e890d29 100644 --- a/macros/src/declare_derive.rs +++ b/macros/src/declare_derive.rs @@ -546,6 +546,13 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result(mut self, v: impl DeclareInto, _M>) -> Self + { + self.fat_obj = self.fat_obj.tooltips(v); + self + } } } }; diff --git a/macros/src/variable_names.rs b/macros/src/variable_names.rs index ac10ed392..ab5fe2b7c 100644 --- a/macros/src/variable_names.rs +++ b/macros/src/variable_names.rs @@ -160,6 +160,8 @@ pub static BUILTIN_INFOS: phf::Map<&'static str, BuiltinMember> = phf_map! { "opacity" => builtin_member!{"Opacity", Field, "opacity"}, // KeepAlive "keep_alive" => builtin_member!{"KeepAlive", Field, "keep_alive"}, + // Tooltips + "tooltips" => builtin_member!{"Tooltips", Field, "tooltips"}, // TrackWidgetId "track_id" => builtin_member!{"TrackWidgetId", Method, "track_id"}, }; diff --git a/test_cases/ribir_widgets/text/tests/default_text_with_default_by_wgpu.png b/test_cases/ribir_core/builtin_widgets/text/tests/default_text_with_default_by_wgpu.png similarity index 100% rename from test_cases/ribir_widgets/text/tests/default_text_with_default_by_wgpu.png rename to test_cases/ribir_core/builtin_widgets/text/tests/default_text_with_default_by_wgpu.png diff --git a/test_cases/ribir_widgets/text/tests/default_text_with_material_by_wgpu.png b/test_cases/ribir_core/builtin_widgets/text/tests/default_text_with_material_by_wgpu.png similarity index 100% rename from test_cases/ribir_widgets/text/tests/default_text_with_material_by_wgpu.png rename to test_cases/ribir_core/builtin_widgets/text/tests/default_text_with_material_by_wgpu.png diff --git a/test_cases/ribir_widgets/text/tests/h1_with_default_by_wgpu.png b/test_cases/ribir_core/builtin_widgets/text/tests/h1_with_default_by_wgpu.png similarity index 100% rename from test_cases/ribir_widgets/text/tests/h1_with_default_by_wgpu.png rename to test_cases/ribir_core/builtin_widgets/text/tests/h1_with_default_by_wgpu.png diff --git a/test_cases/ribir_widgets/text/tests/h1_with_material_by_wgpu.png b/test_cases/ribir_core/builtin_widgets/text/tests/h1_with_material_by_wgpu.png similarity index 100% rename from test_cases/ribir_widgets/text/tests/h1_with_material_by_wgpu.png rename to test_cases/ribir_core/builtin_widgets/text/tests/h1_with_material_by_wgpu.png diff --git a/test_cases/ribir_widgets/text/tests/text_clip_with_default_by_wgpu.png b/test_cases/ribir_core/builtin_widgets/text/tests/text_clip_with_default_by_wgpu.png similarity index 100% rename from test_cases/ribir_widgets/text/tests/text_clip_with_default_by_wgpu.png rename to test_cases/ribir_core/builtin_widgets/text/tests/text_clip_with_default_by_wgpu.png diff --git a/test_cases/ribir_widgets/text/tests/text_clip_with_material_by_wgpu.png b/test_cases/ribir_core/builtin_widgets/text/tests/text_clip_with_material_by_wgpu.png similarity index 100% rename from test_cases/ribir_widgets/text/tests/text_clip_with_material_by_wgpu.png rename to test_cases/ribir_core/builtin_widgets/text/tests/text_clip_with_material_by_wgpu.png diff --git a/themes/material/src/classes.rs b/themes/material/src/classes.rs index 5eaa3f47d..f07b2e1cf 100644 --- a/themes/material/src/classes.rs +++ b/themes/material/src/classes.rs @@ -5,6 +5,7 @@ mod icon_cls; mod progress_cls; mod radio_cls; mod scrollbar_cls; +mod tooltips_cls; pub fn initd_classes() -> Classes { let mut classes = Classes::default(); @@ -14,6 +15,7 @@ pub fn initd_classes() -> Classes { radio_cls::init(&mut classes); progress_cls::init(&mut classes); checkbox_cls::init(&mut classes); + tooltips_cls::init(&mut classes); classes } diff --git a/themes/material/src/classes/tooltips_cls.rs b/themes/material/src/classes/tooltips_cls.rs new file mode 100644 index 000000000..acdf8c03c --- /dev/null +++ b/themes/material/src/classes/tooltips_cls.rs @@ -0,0 +1,52 @@ +use ribir_core::prelude::*; + +class_names! { + TOOLTIPS_BASE, +} + +pub(super) fn init(classes: &mut Classes) { + classes.insert(TOOLTIPS_BASE, |w| { + fn_widget! { + let w = FatObj::new(w); + @BoxDecoration { + background: Palette::of(BuildCtx::get()).inverse_surface(), + margin: EdgeInsets::only_bottom(4.), + border_radius: Radius::all(4.), + @ $w { + margin: EdgeInsets::new(4., 8., 4., 8.), + foreground: Palette::of(BuildCtx::get()).inverse_on_surface(), + v_align: VAlign::Center, + h_align: HAlign::Center, + } + } + } + .into_widget() + }); + + classes.insert(TOOLTIPS, |w| { + fn_widget! { + let w = FatObj::new(w); + let mut w = @ $w { + class: TOOLTIPS_BASE, + }; + $w.write().keep_alive = true; + + let animate = part_writer!(&mut w.opacity) + .transition(transitions::LINEAR.of(BuildCtx::get())); + @ $w { + on_disposed: move |_| { + $w.write().opacity = 0.; + let _ = watch!($animate.is_running()) + .distinct_until_changed() + .pairwise() + .filter(|(old, new)| *old && !*new ) + .take(1) + .subscribe(move|_| { + $w.write().keep_alive = false; + }); + } + } + } + .into_widget() + }); +} diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index ee4909813..b78d7e9b1 100644 --- a/widgets/src/checkbox.rs +++ b/widgets/src/checkbox.rs @@ -3,8 +3,7 @@ use svg::named_svgs; use crate::{ common_widget::{Leading, Trailing}, - prelude::{Icon, Row, Text}, - text::TextInit, + prelude::{Icon, Row}, }; class_names! { diff --git a/widgets/src/icon.rs b/widgets/src/icon.rs index 1ae714e2f..b090956d1 100644 --- a/widgets/src/icon.rs +++ b/widgets/src/icon.rs @@ -1,7 +1,5 @@ use ribir_core::prelude::*; -use crate::text::*; - /// An icon widget represents an icon. /// /// It can accept either text or another widget as its child. If the child is diff --git a/widgets/src/input.rs b/widgets/src/input.rs index 8f830dad4..268139891 100644 --- a/widgets/src/input.rs +++ b/widgets/src/input.rs @@ -19,7 +19,6 @@ use crate::{ text_selectable::{SelectableText, bind_point_listener, select_key_handle}, }, layout::{OnlySizedByParent, Stack, StackFit}, - prelude::Text, }; pub struct Placeholder(DeclareInit>); diff --git a/widgets/src/layout/sized_box.rs b/widgets/src/layout/sized_box.rs index df03c7529..c488636e3 100644 --- a/widgets/src/layout/sized_box.rs +++ b/widgets/src/layout/sized_box.rs @@ -28,7 +28,6 @@ mod tests { use ribir_dev_helper::*; use super::*; - use crate::prelude::*; widget_layout_test!( fix_size, diff --git a/widgets/src/lib.rs b/widgets/src/lib.rs index 3d24b758a..ab5b0f5a7 100644 --- a/widgets/src/lib.rs +++ b/widgets/src/lib.rs @@ -15,13 +15,13 @@ pub mod progress; pub mod radio; pub mod scrollbar; pub mod tabs; -pub mod text; pub mod text_field; + pub mod transform_box; pub mod prelude { pub use super::{ avatar::*, buttons::*, checkbox::*, common_widget::*, divider::*, grid_view::*, icon::*, input::*, label::*, layout::*, link::*, lists::*, path::*, progress::*, radio::*, scrollbar::*, - tabs::*, text::*, text_field::*, transform_box::*, + tabs::*, text_field::*, transform_box::*, }; }