diff --git a/CHANGELOG.md b/CHANGELOG.md index 2146d3125..170f87be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he - **core**: Enhanced support for built-in fields such as `font_size`, `font_face`, `letter_spacing`, `text_line_height`, and `text_overflow` through `TextStyleWidget`. (#668 @M-Adoo) - **widgets**: Icon size should be maintained even if its container is not sufficiently large. (#668 @M-Adoo) +- **core**: Added the builtin widget of tooltips (#pr @wjian23) ### Changed diff --git a/core/Cargo.toml b/core/Cargo.toml index 60a73bedd..2d2992be0 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,8 @@ web-time.workspace = true colored.workspace = true paste.workspace = true ribir_dev_helper = {path = "../dev-helper"} -ribir = { path = "../ribir" } +ribir = { path = "../ribir", features = ["material"] } +ribir_slim = { path = "../themes/ribir_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 07e1d1392..5f8c3c9d4 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 { @@ -857,6 +872,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(); @@ -944,6 +964,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 95% rename from widgets/src/text.rs rename to core/src/builtin_widgets/text.rs index 2a1dafe91..7b875917a 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::*}; 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..0b77a9f79 --- /dev/null +++ b/core/src/builtin_widgets/tooltips.rs @@ -0,0 +1,105 @@ +use std::cell::RefCell; + +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, + + overlay: RefCell>, +} + +impl Declare for Tooltips { + type Builder = FatObj<()>; + fn declarer() -> Self::Builder { FatObj::new(()) } +} + +impl Tooltips { + fn tooltips(&self) -> &CowArc { &self.tooltips } + + pub fn show(&self, wnd: Sc) { + if let Some(overlay) = self.overlay.borrow().clone() { + if !overlay.is_showing() { + overlay.show(wnd); + } + } + } + + pub fn hidden(&self) { + if let Some(overlay) = self.overlay.borrow().clone() { + if overlay.is_showing() { + overlay.close(); + } + } + } +} + +impl<'c> ComposeChild<'c> for Tooltips { + type Child = Widget<'c>; + fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { + fn_widget! { + let mut child = FatObj::new(child); + *$this.overlay.borrow_mut() = Some(Overlay::new( + move || { + let w = @Text { + text: pipe!($this.tooltips().clone()), + class: TOOLTIPS, + }; + + @ $w { + global_anchor_x: pipe!( + GlobalAnchorX::center_align_to( + $child.track_id(), 0. + ).always_follow() + ), + global_anchor_y: pipe!( + GlobalAnchorY::bottom_align_to( + $child.track_id(), $child.layout_size().height + ).always_follow() + ), + }.into_widget() + }, OverlayStyle { + auto_close_policy: AutoClosePolicy::NOT_AUTO_CLOSE, + mask: None, + } + )); + + let wnd = BuildCtx::get().window(); + let u = watch!($child.is_hover()) + .distinct_until_changed() + .subscribe(move |v| { + if v { + $this.show(wnd.clone()); + } else { + $this.hidden(); + } + }); + + @ $child { + on_disposed: move|_| { + u.unsubscribe(); + $this.hidden(); + }, + } + } + .into_widget() + } +} diff --git a/core/src/overlay.rs b/core/src/overlay.rs index bcfa83dc0..07929096c 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -214,6 +214,7 @@ impl Overlay { } } }, + @{ w } }.into_widget(); }; if close_policy.contains(AutoClosePolicy::ESC) { diff --git a/macros/src/declare_derive.rs b/macros/src/declare_derive.rs index 33a53f100..b86f92161 100644 --- a/macros/src/declare_derive.rs +++ b/macros/src/declare_derive.rs @@ -587,6 +587,12 @@ 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 298f6370b..7efc85d50 100644 --- a/macros/src/variable_names.rs +++ b/macros/src/variable_names.rs @@ -166,6 +166,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 7ad99fcbf..3d3f93eaf 100644 --- a/themes/material/src/classes.rs +++ b/themes/material/src/classes.rs @@ -4,6 +4,7 @@ mod checkbox_cls; mod progress_cls; mod radio_cls; mod scrollbar_cls; +mod tooltips_cls; pub fn initd_classes() -> Classes { let mut classes = Classes::default(); @@ -12,6 +13,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..f705e483f --- /dev/null +++ b/themes/material/src/classes/tooltips_cls.rs @@ -0,0 +1,34 @@ +use ribir_core::prelude::*; + +use crate::md; + +pub(super) fn init(classes: &mut Classes) { + classes.insert(TOOLTIPS, |w| { + fn_widget! { + let w = FatObj::new(w); + let mut 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, + } + }; + let animate = part_writer!(&mut w.opacity) + .transition(EasingTransition{ + easing: md::easing::STANDARD_ACCELERATE, + duration: md::easing::duration::SHORT2 + }.box_it()); + @ $w { + keep_alive: pipe!($animate.is_running() || $w.opacity != 0.), + on_disposed: move |_| { + $w.write().opacity = 0.; + } + } + } + .into_widget() + }); +} diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index f9a2cfbad..5942c5019 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 f48244c26..639cb9540 100644 --- a/widgets/src/icon.rs +++ b/widgets/src/icon.rs @@ -2,8 +2,6 @@ use std::cell::Cell; use ribir_core::{impl_compose_child_for_wrap_render, prelude::*, wrap_render::WrapRender}; -use crate::text::*; - /// An widget represents an icon. /// /// The icon size is determined by the text line height, so you can use diff --git a/widgets/src/input.rs b/widgets/src/input.rs index 59f3ae159..4d4409f1d 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::*, }; }