diff --git a/examples/widget_gallery.rs b/examples/widget_gallery.rs new file mode 100644 index 0000000..80c86e3 --- /dev/null +++ b/examples/widget_gallery.rs @@ -0,0 +1,46 @@ +// Counter UI example. + +use actuate::prelude::*; +use bevy::{asset::io::SeekForwardFuture, prelude::*}; + +// Counter composable. +#[derive(Data)] +struct WidgetGallery; + +impl Compose for WidgetGallery { + fn compose(cx: Scope) -> impl Compose { + let is_shown = use_mut(&cx, || true); + // let selected_value = use_mut(&cx, || 5.); + + material_ui(( + radio_button().is_enabled(*is_shown), + switch() + .is_enabled(*is_shown) + .on_click(move || SignalMut::update(is_shown, |x| *x = !*x)), + // slider().current(*selected_value), + )) + .width(Val::Vw(100.)) + } +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + // Spawn a composition with a `Counter`, adding it to the Actuate runtime. + commands.spawn(( + Node { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + row_gap: Val::Px(10.), + ..default() + }, + Composition::new(WidgetGallery), + )); +} + +fn main() { + App::new() + .add_plugins((DefaultPlugins, ActuatePlugin)) + .add_systems(Startup, setup) + .run(); +} diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs index bde1743..f083a67 100644 --- a/src/ecs/mod.rs +++ b/src/ecs/mod.rs @@ -53,7 +53,7 @@ pub struct ActuatePlugin; impl Plugin for ActuatePlugin { fn build(&self, app: &mut App) { - let rt = Runtime { + let rt = BevyRuntime { composers: RefCell::new(HashMap::new()), }; @@ -102,7 +102,7 @@ struct RuntimeComposer { composer: Composer, } -struct Runtime { +struct BevyRuntime { composers: RefCell>, } @@ -161,7 +161,7 @@ where let content = composition.content.take().unwrap(); let target = composition.target.unwrap_or(entity); - let rt = world.non_send_resource_mut::(); + let rt = world.non_send_resource_mut::(); rt.composers.borrow_mut().insert( entity, @@ -237,7 +237,7 @@ fn compose(world: &mut World) { .get_resource::>() .unwrap()) .clone(); - let rt = &mut *world.non_send_resource_mut::(); + let rt = &mut *world.non_send_resource_mut::(); let mut composers = rt.composers.borrow_mut(); for rt_composer in composers.values_mut() { let waker = Waker::from(Arc::new(RuntimeWaker { diff --git a/src/lib.rs b/src/lib.rs index 6edbc56..28658f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,8 +158,8 @@ pub mod prelude { #[cfg(feature = "material")] #[cfg_attr(docsrs, doc(cfg(feature = "material")))] pub use crate::ui::material::{ - button, container, material_ui, radio_button, text, Button, MaterialUi, RadioButton, Theme, - TypographyKind, TypographyStyleKind, + button, container, material_ui, radio_button, switch, text, Button, MaterialUi, + RadioButton, Switch, Theme, TypographyKind, TypographyStyleKind, }; } @@ -403,7 +403,7 @@ impl<'a, T> Signal<'a, T> { ptr: me.value as *const _ as _, map_fn: f as _, deref_fn: |ptr, g| { - // Safety: `f` is guranteed to be a valid function pointer. + // Safety: `f` is guaranteed to be a valid function pointer. unsafe { let g: fn(&T) -> &U = mem::transmute(g); g(&*(ptr as *const T)) @@ -494,7 +494,7 @@ impl<'a, T: 'static> SignalMut<'a, T> { } } - /// Queue an update to this value wtihout triggering an update. + /// Queue an update to this value without triggering an update. pub fn with(me: Self, f: impl FnOnce(&mut T) + Send + 'static) { let cell = UnsafeWrap(Some(f)); let ptr = UnsafeWrap(me.ptr); diff --git a/src/ui/material/mod.rs b/src/ui/material/mod.rs index c155aaa..875c8bf 100644 --- a/src/ui/material/mod.rs +++ b/src/ui/material/mod.rs @@ -13,6 +13,12 @@ pub use self::radio::{radio_button, RadioButton}; mod ui; pub use self::ui::{material_ui, MaterialUi}; +mod switch; +pub use self::switch::{switch, Switch}; + +// mod slider; +// pub use self::slider::{slider, Slider}; + /// Text composables. pub mod text; diff --git a/src/ui/material/radio.rs b/src/ui/material/radio.rs index 3aef23e..dfee108 100644 --- a/src/ui/material/radio.rs +++ b/src/ui/material/radio.rs @@ -70,7 +70,8 @@ impl Compose for RadioButton<'_> { let size = Val::Px(cx.me().outer_radius * 2.); let inner_size = Val::Px(cx.me().inner_radius * 2.); - let offset = Val::Px((cx.me().outer_radius - cx.me().inner_radius) - 2.); + let padding = Val::Px((cx.me().outer_radius - cx.me().inner_radius) - 2.); + let padding_rect = UiRect::all(padding); cx.me() .modifier @@ -79,6 +80,7 @@ impl Compose for RadioButton<'_> { width: size, height: size, border: UiRect::all(Val::Px(cx.me().border_width)), + padding: padding_rect, ..Default::default() }, BorderRadius::MAX, @@ -96,8 +98,6 @@ impl Compose for RadioButton<'_> { Node { width: inner_size, height: inner_size, - top: offset, - left: offset, ..Default::default() }, diff --git a/src/ui/material/switch.rs b/src/ui/material/switch.rs new file mode 100644 index 0000000..abd54f3 --- /dev/null +++ b/src/ui/material/switch.rs @@ -0,0 +1,122 @@ +use crate::ui::material::Theme; +use crate::{ + compose::Compose, + ecs::spawn, + ecs::{Modifier, Modify}, + use_context, Data, Scope, +}; +use bevy_color::Color; +use bevy_ui::{BackgroundColor, BorderColor, BorderRadius, BoxShadow, Node, UiRect, Val}; + +/// Create a material UI switch. +pub fn switch<'a>() -> Switch<'a> { + Switch { + is_enabled: true, + inner_radius: 10., + outer_radius: 20., + border_width: 2., + elevation: 0., + modifier: Modifier::default(), + } +} + +/// Material UI radio button. +#[derive(Clone, Debug, Data)] +#[actuate(path = "crate")] +pub struct Switch<'a> { + is_enabled: bool, + inner_radius: f32, + outer_radius: f32, + border_width: f32, + elevation: f32, + modifier: Modifier<'a>, +} + +impl Switch<'_> { + /// Set the enabled state of this radio button. + pub fn is_enabled(mut self, is_enabled: bool) -> Self { + self.is_enabled = is_enabled; + self + } + + /// Set the inner radius of this radio button. + pub fn inner_radius(mut self, inner_radius: f32) -> Self { + self.inner_radius = inner_radius; + self + } + + /// Set the outer radius of this radio button. + pub fn outer_radius(mut self, outer_radius: f32) -> Self { + self.outer_radius = outer_radius; + self + } + + /// Set the border width of this radio button. + pub fn border_width(mut self, border_width: f32) -> Self { + self.border_width = border_width; + self + } + + /// Set the elevation of this radio button. + pub fn elevation(mut self, elevation: f32) -> Self { + self.elevation = elevation; + self + } +} + +impl Compose for Switch<'_> { + fn compose(cx: Scope) -> impl Compose { + let theme = use_context::(&cx).cloned().unwrap_or_default(); + + let height = Val::Px(cx.me().outer_radius * 2.); + let width = Val::Px(cx.me().outer_radius * 3.); + let knob_size = Val::Px(cx.me().inner_radius * 2.); + let padding = (cx.me().outer_radius - cx.me().inner_radius) - 2.; + let padding_right = padding + 2. * cx.me().inner_radius; + let mut padding_rect = UiRect::all(Val::Px(padding)); + padding_rect.right = Val::Px(padding_right); + let offset_left_val = if cx.me().is_enabled { + Val::Percent(0.) + } else { + Val::Percent(100.) + }; + + cx.me() + .modifier + .apply(spawn(( + Node { + width, + height, + border: UiRect::all(Val::Px(cx.me().border_width)), + padding: padding_rect, + ..Default::default() + }, + BorderRadius::MAX, + BorderColor(theme.colors.primary), + BoxShadow { + color: Color::srgba(0., 0., 0., 0.12 * cx.me().elevation), + x_offset: Val::Px(0.), + y_offset: Val::Px(1.), + spread_radius: Val::Px(0.), + blur_radius: Val::Px(3. * cx.me().elevation), + }, + ))) + .content(Some(spawn(( + Node { + width: knob_size, + height: knob_size, + left: offset_left_val, + + ..Default::default() + }, + BackgroundColor(theme.colors.primary), + BorderRadius::MAX, + )))) + } +} + +impl<'a> Modify<'a> for Switch<'a> { + fn modifier(&mut self) -> &mut Modifier<'a> { + &mut self.modifier + } +}