Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Class to enable the sharing of consistent styles across multiple elements #624

Merged
merged 8 commits into from
Sep 10, 2024
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he

## [@Unreleased] - @ReleaseDate

### Features

- **core**: The built-in widget `Class` has been added to enable the sharing of consistent styles across multiple elements and to allow widgets to have different actions and styles in different themes. (#624, @M-Adoo)
- **core**: The widget `ConstrainedBox` has been added as a built-in widget; now `clamp` can be used as a built-in field. (#624 @M-Adoo)
- **core**: Added `WindowFlags` to regulate the window behavior, with the option of utilizing `WindowFlags::ANIMATIONS` to toggle animations on or off. (#624 @M-Adoo)
- **theme/material**: Define the constant variables of motion. (#624, @M-Adoo)
- **dev_helper**: Refine the widget test macros. (#624, @M-Adoo)

### Changed

- **widgets**: Utilize `Class` to implement the `Scrollbar`. (#624, @M-Adoo)

### Breaking

- **widgets**: `ConstrainedBox` has been relocated to `core`. (#624, @M-Adoo)
- **widgets**: Utilize `Scrollbar` instead of both `HScrollbar`, `VScrollbar`, and `BothScrollbar`. (#624, @M-Adoo)

### Fixed

- **macros**: Declaring the variable parent with built-in fields as immutable is incorrect if its child uses it as mutable. (#623 @M-Adoo)
Expand All @@ -35,6 +52,7 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he

- **widgets**: Flex may not decrease the gap for the second child during layout. (#622 @M-Adoo)


## [0.4.0-alpha.6] - 2024-08-21

### Features
Expand Down
4 changes: 2 additions & 2 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ This document outlines the release process and branch management for the Ribir p
|---------|-----------|--------------|--------|---------|---------|
| 0.1 | Prototype, Validate our ideas | Feb 2024 | Stable | [M1](https://github.com/RibirX/Ribir/milestone/1) | @M-Adoo |
| 0.2 | Improve Core API | March 2024 | Stable | [M2](https://github.com/RibirX/Ribir/milestone/2) | @M-Adoo |
| 0.3 | Web Compatibility and Widget System API Stabilization | May 2024 | Beta | [M3](https://github.com/RibirX/Ribir/milestone/3) | @wjian23 |
| 0.4 | Simplified Theme and Widget Development | June 2024 | Alpha | [M4](https://github.com/RibirX/Ribir/milestone/3) | @sologeek |
| 0.3 | Web Compatibility and Widget System API Stabilization | Aug 2024 | Beta | [M3](https://github.com/RibirX/Ribir/milestone/3) | @wjian23 |
| 0.4 | Simplified Theme and Widget Development | Sep 2024 | Alpha | [M4](https://github.com/RibirX/Ribir/milestone/3) | @sologeek |
| 0.5 | Widgets and Storybook | To be determined | Planning | To be determined | To be determined |
| Future | To be determined | To be determined | Planning | To be determined | To be determined |

Expand Down
14 changes: 8 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,20 @@ This milestone aims to prepare Ribir for the web and stabilize the widget system

This milestone aims to stabilize the theme API, simplify type conversion, and facilitate the development of widgets with dynamic themes.

- [ ] Simplify type conversion.
- [x] Simplify type conversion.
We've over-engineered some aspects of type conversion, which has actually increased the learning curve for users and reduced error readability. For instance, the conversion of `DeclareInit` and the nested conversion of `Template`. The downside is that more explicit conversions will be required when using them.
- [ ] In-depth widget guide, explaining how widgets work and how to create custom widgets.
- [ ] Implement a provider widget that can exports a state to its subtree, allowing widgets in its subtree to query the state using context.
- [ ] Simplify the theme system API to enhance user-friendliness.
- [x] Implement a provider widget that can exports a state to its subtree, allowing widgets in its subtree to query the state using context.
- [x] Simplify the theme system API to enhance user-friendliness.
- [ ] Include additional built-in paint style widgets that will be inherited by descendants, such as `TextStyle` and `Foreground`.
- [ ] Implement a mechanism to enable sharing styles between widgets, akin to the `class` attribute in HTML.

### Widgets Library And Storybook (v0.5, July 2024)
## Widgets Library And Storybook (v0.5)

- [ ] In-depth widget guide, explaining how widgets work and how to create custom widgets.
- [ ] Production level widgets library with the basic widgets
- [ ] Complete the basic and material themes
- [ ] storybook to display all widgets, allowing user interaction
- [ ] mobile platform basic support (iOS, Android)


## Backlog

Expand Down
14 changes: 12 additions & 2 deletions core/src/animation/animate.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::{prelude::*, ticker::FrameMsg, window::WindowId};
use crate::{
prelude::*,
ticker::FrameMsg,
window::{WindowFlags, WindowId},
};
#[simple_declare]
pub struct Animate<S>
where
Expand Down Expand Up @@ -35,14 +39,20 @@ where
let mut animate_ref = self.write();
let this = &mut *animate_ref;
let wnd_id = this.window_id;
let Some(wnd) = AppCtx::get_window(this.window_id) else { return };

if !wnd.flags().contains(WindowFlags::ANIMATIONS) {
return;
}

let new_to = this.state.get();

if let Some(AnimateInfo { from, to, last_progress, .. }) = &mut this.running_info {
*from = this
.state
.calc_lerp_value(from, to, last_progress.value());
*to = new_to;
} else if let Some(wnd) = AppCtx::get_window(wnd_id) {
} else {
drop(animate_ref);

let animate = self.clone_writer();
Expand Down
7 changes: 3 additions & 4 deletions core/src/animation/stagger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ mod tests {

widget_layout_test!(
stagger_run_and_stop,
fn_widget! {
WidgetTester::new(fn_widget! {
let stagger = Stagger::new(Duration::from_millis(100), transitions::EASE_IN.of(ctx!()));
let mut mock_box = @MockBox { size: Size::new(100., 100.) };
let opacity = mock_box
Expand All @@ -253,9 +253,8 @@ mod tests {
assert!(!stagger.is_running());

mock_box
},
width == 100.,
height == 100.,
}),
LayoutCase::default().with_size(Size::new(100., 100.))
);

#[test]
Expand Down
6 changes: 4 additions & 2 deletions core/src/animation/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ pub struct RepeatTransition<T> {
/// Trait help to transition the state.
pub trait TransitionState: Sized + 'static {
/// Use an animate to transition the state after it modified.
fn transition(self, transition: Box<dyn Transition>, ctx: &BuildCtx) -> Stateful<Animate<Self>>
fn transition(
self, transition: impl Transition + 'static, ctx: &BuildCtx,
) -> Stateful<Animate<Self>>
where
Self: AnimateState,
{
let state = self.clone_setter();
let animate = Animate::declarer()
.transition(transition)
.transition(Box::new(transition))
.from(self.get())
.state(self)
.finish(ctx);
Expand Down
164 changes: 71 additions & 93 deletions core/src/builtin_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ pub mod container;
pub use container::*;
mod provider;
pub use provider::*;
mod class;
pub use class::*;
mod constrained_box;
pub use constrained_box::*;

use crate::prelude::*;

Expand Down Expand Up @@ -99,6 +103,7 @@ pub struct LazyWidgetId(Sc<Cell<Option<WidgetId>>>);
/// .into_widget()
/// };
/// ```
#[derive(Default)]
pub struct FatObj<T> {
host: T,
host_id: LazyWidgetId,
Expand All @@ -115,13 +120,15 @@ pub struct FatObj<T> {
cursor: Option<State<Cursor>>,
margin: Option<State<Margin>>,
scrollable: Option<State<ScrollableWidget>>,
constrained_box: Option<State<ConstrainedBox>>,
transform: Option<State<TransformWidget>>,
h_align: Option<State<HAlignWidget>>,
v_align: Option<State<VAlignWidget>>,
relative_anchor: Option<State<RelativeAnchor>>,
global_anchor: Option<State<GlobalAnchor>>,
visibility: Option<State<Visibility>>,
opacity: Option<State<Opacity>>,
class: Option<State<Class>>,
keep_alive: Option<State<KeepAlive>>,
keep_alive_unsubscribe_handle: Option<Box<dyn Any>>,
}
Expand Down Expand Up @@ -157,34 +164,7 @@ impl LazyWidgetId {

impl<T> FatObj<T> {
/// Create a new `FatObj` with the given host object.
pub fn new(host: T) -> Self {
Self {
host,
host_id: LazyWidgetId::default(),
id: LazyWidgetId::default(),
mix_builtin: None,
request_focus: None,
has_focus: None,
mouse_hover: None,
pointer_pressed: None,
fitted_box: None,
box_decoration: None,
padding: None,
layout_box: None,
cursor: None,
margin: None,
scrollable: None,
transform: None,
h_align: None,
v_align: None,
relative_anchor: None,
global_anchor: None,
visibility: None,
opacity: None,
keep_alive: None,
keep_alive_unsubscribe_handle: None,
}
}
pub fn new(host: T) -> Self { FatObj::<()>::default().with_child(host) }

/// Maps an `FatObj<T>` to `FatObj<V>` by applying a function to the host
/// object.
Expand All @@ -206,11 +186,13 @@ impl<T> FatObj<T> {
cursor: self.cursor,
margin: self.margin,
scrollable: self.scrollable,
constrained_box: self.constrained_box,
transform: self.transform,
h_align: self.h_align,
v_align: self.v_align,
relative_anchor: self.relative_anchor,
global_anchor: self.global_anchor,
class: self.class,
visibility: self.visibility,
opacity: self.opacity,
keep_alive: self.keep_alive,
Expand Down Expand Up @@ -265,6 +247,14 @@ impl<T> FatObj<T> {

// builtin widgets accessors
impl<T> FatObj<T> {
/// Returns the `State<Class>` widget from the FatObj. If it doesn't exist, a
/// new one is created.
pub fn get_class_widget(&mut self) -> &mut State<Class> {
self
.class
.get_or_insert_with(|| State::value(<_>::default()))
}

pub fn get_mix_builtin_widget(&mut self) -> &mut State<MixBuiltin> {
self
.mix_builtin
Expand Down Expand Up @@ -351,6 +341,12 @@ impl<T> FatObj<T> {
.get_or_insert_with(|| State::value(<_>::default()))
}

pub fn get_constrained_box_widget(&mut self) -> &mut State<ConstrainedBox> {
self
.constrained_box
.get_or_insert_with(|| State::value(<_>::default()))
}

/// Returns the `State<ScrollableWidget>` widget from the FatObj. If it
/// doesn't exist, a new one is created.
pub fn get_scrollable_widget(&mut self) -> &mut State<ScrollableWidget> {
Expand Down Expand Up @@ -724,6 +720,11 @@ impl<T> FatObj<T> {
})
}

/// Initializes the `Class` that should be applied to the widget.
pub fn class<const M: u8>(self, cls: impl DeclareInto<ClassName, M>) -> Self {
self.declare_builtin_init(cls, Self::get_class_widget, |c, cls| c.class = Some(cls))
}

/// Initializes whether the `widget` should automatically get focus when the
/// window loads.
///
Expand Down Expand Up @@ -770,16 +771,16 @@ impl<T> FatObj<T> {
self.declare_builtin_init(v, Self::get_margin_widget, |m, v| m.margin = v)
}

/// Initializes the constraints clamp of the widget.
pub fn clamp<const M: u8>(self, v: impl DeclareInto<BoxClamp, M>) -> Self {
self.declare_builtin_init(v, Self::get_constrained_box_widget, |m, v| m.clamp = v)
}

/// Initializes how user can scroll the widget.
pub fn scrollable<const M: u8>(self, v: impl DeclareInto<Scrollable, M>) -> Self {
self.declare_builtin_init(v, Self::get_scrollable_widget, |m, v| m.scrollable = v)
}

/// Initializes the position of the widget's scroll.
pub fn scroll_pos<const M: u8>(self, v: impl DeclareInto<Point, M>) -> Self {
self.declare_builtin_init(v, Self::get_scrollable_widget, |m, v| m.scroll_pos = v)
}

/// Initializes the transformation of the widget.
pub fn transform<const M: u8>(self, v: impl DeclareInto<Transform, M>) -> Self {
self.declare_builtin_init(v, Self::get_transform_widget, |m, v| m.transform = v)
Expand Down Expand Up @@ -871,70 +872,47 @@ where

impl<'a> FatObj<Widget<'a>> {
fn compose(self) -> Widget<'a> {
macro_rules! compose_builtin_widgets {
($host: ident + [$($field: ident),*]) => {
$(
if let Some($field) = self.$field {
$host = $field.with_child($host).into_widget();
}
)*
};
}
let mut host = self.host;
if self.host_id.0.ref_count() > 1 {
host = self.host_id.clone().bind(host);
}
if let Some(mix_builtin) = self.mix_builtin {
host = mix_builtin.with_child(host).into_widget()
}
if let Some(request_focus) = self.request_focus {
host = request_focus.with_child(host).into_widget();
}
if let Some(has_focus) = self.has_focus {
host = has_focus.with_child(host).into_widget();
}
if let Some(mouse_hover) = self.mouse_hover {
host = mouse_hover.with_child(host).into_widget();
}
if let Some(pointer_pressed) = self.pointer_pressed {
host = pointer_pressed.with_child(host).into_widget();
}
if let Some(fitted_box) = self.fitted_box {
host = fitted_box.with_child(host).into_widget();
}
if let Some(box_decoration) = self.box_decoration {
host = box_decoration.with_child(host).into_widget();
}
if let Some(padding) = self.padding {
host = padding.with_child(host).into_widget();
}
if let Some(layout_box) = self.layout_box {
host = layout_box.with_child(host).into_widget();
}
if let Some(cursor) = self.cursor {
host = cursor.with_child(host).into_widget();
}
if let Some(margin) = self.margin {
host = margin.with_child(host).into_widget();
}
if let Some(scrollable) = self.scrollable {
host = scrollable.with_child(host).into_widget();
}
if let Some(transform) = self.transform {
host = transform.with_child(host).into_widget();
}
if let Some(h_align) = self.h_align {
host = h_align.with_child(host).into_widget();
}
if let Some(v_align) = self.v_align {
host = v_align.with_child(host).into_widget();
}
if let Some(relative_anchor) = self.relative_anchor {
host = relative_anchor.with_child(host).into_widget();
}
if let Some(global_anchor) = self.global_anchor {
host = global_anchor.with_child(host).into_widget();
}
if let Some(visibility) = self.visibility {
host = visibility.with_child(host).into_widget();
}
if let Some(opacity) = self.opacity {
host = opacity.with_child(host).into_widget();
}
if let Some(keep_alive) = self.keep_alive {
host = keep_alive.with_child(host).into_widget();
}
compose_builtin_widgets!(
host
+ [
padding,
fitted_box,
constrained_box,
box_decoration,
scrollable,
layout_box,
mix_builtin,
request_focus,
has_focus,
mouse_hover,
pointer_pressed,
cursor,
margin,
transform,
visibility,
opacity,
class,
h_align,
v_align,
relative_anchor,
global_anchor,
keep_alive
]
);

if let Some(h) = self.keep_alive_unsubscribe_handle {
host = host.attach_anonymous_data(h);
}
Expand Down
Loading
Loading