diff --git a/examples/sliders.rs b/examples/sliders.rs new file mode 100644 index 0000000..e4a49a6 --- /dev/null +++ b/examples/sliders.rs @@ -0,0 +1,48 @@ +#![feature(type_alias_impl_trait, impl_trait_in_assoc_type)] + +use nuit::{prelude::*, Circle, Font, FontDesign, FontSize, Frame, HStack, Slider, Text, VStack, Vec2}; + +#[derive(Bind, Default)] +struct SlidersView { + position: State>, +} + +impl View for SlidersView { + type Body = impl View; + + #[allow(clippy::cast_possible_truncation)] + fn body(&self) -> Self::Body { + let position = self.position.clone(); + let width = 400.0; + let height = 300.0; + let slider_width = 100.0; + + VStack::from(( + Circle::new() + .frame(10) + .offset(position.get()) + .frame((width, height)), + + HStack::from(( + Text::new(format!("X: {:>4}", position.get().x as i32)), + Slider::with_default_step( + position.project(|p| &mut p.x), + -(width / 2.0)..=(width / 2.0) + ) + .frame(Frame::with_width(slider_width)), + + Text::new(format!("Y: {:>4}", position.get().y as i32)), + Slider::with_default_step( + position.project(|p| &mut p.y), + -(height / 2.0)..=(height / 2.0) + ) + .frame(Frame::with_width(slider_width)), + ) + .font(Font::system(FontSize::BODY, FontDesign::Monospaced, None))), + )) + } +} + +fn main() { + nuit::run_app(SlidersView::default()); +} diff --git a/nuit-bridge-adwaita/src/node_widget/mod.rs b/nuit-bridge-adwaita/src/node_widget/mod.rs index f098639..90f920f 100644 --- a/nuit-bridge-adwaita/src/node_widget/mod.rs +++ b/nuit-bridge-adwaita/src/node_widget/mod.rs @@ -2,7 +2,7 @@ mod imp; use std::rc::Rc; -use adw::{glib::{self, Object}, gtk::{self, Align, Button, Label, Orientation, Text}, prelude::{BoxExt, ButtonExt, EditableExt, WidgetExt}, subclass::prelude::*}; +use adw::{glib::{self, Object}, gtk::{self, Align, Button, Label, Orientation, Scale, Text}, prelude::{BoxExt, ButtonExt, EditableExt, RangeExt, WidgetExt}, subclass::prelude::*}; use nuit_core::{clone, Event, Id, IdPath, IdPathBuf, Identified, Node}; use crate::convert::ToGtk; @@ -105,6 +105,18 @@ impl NodeWidget { } self.append(&button); }, + Node::Slider { value, lower_bound, upper_bound, step } => { + let scale = Scale::with_range(Orientation::Horizontal, *lower_bound, *upper_bound, step.unwrap_or(1e-32)); + scale.set_value(*value); + scale.set_width_request(150); + if let Some(ref fire_event) = *fire_event { + scale.connect_value_changed(clone!(fire_event, id_path => move |scale| { + let value = scale.value(); + fire_event(&id_path, &Event::UpdateSliderValue { value }); + })); + } + self.append(&scale); + }, Node::HStack { spacing, alignment, wrapped } => { let gtk_box = gtk::Box::new(Orientation::Horizontal, *spacing as i32); gtk_box.set_valign(alignment.to_gtk()); diff --git a/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Event/Event.swift b/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Event/Event.swift index 65b47d8..dcf9847 100644 --- a/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Event/Event.swift +++ b/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Event/Event.swift @@ -3,4 +3,5 @@ enum Event: Codable, Hashable { case gesture(gesture: GestureEvent) case updateText(content: String) case updatePickerSelection(id: Id) + case updateSliderValue(value: Double) } diff --git a/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Node.swift b/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Node.swift index ff7a629..4dcd359 100644 --- a/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Node.swift +++ b/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/Node.swift @@ -6,6 +6,7 @@ indirect enum Node: Codable, Hashable { case textField(content: String) case button(label: Identified) case picker(title: String, selection: Id, content: Identified) + case slider(value: Double, lowerBound: Double, upperBound: Double, step: Double?) // MARK: Aggregation case child(wrapped: Identified) diff --git a/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/NodeView.swift b/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/NodeView.swift index a705827..1878977 100644 --- a/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/NodeView.swift +++ b/nuit-bridge-swiftui/Sources/NuitBridgeSwiftUI/NodeView.swift @@ -34,6 +34,16 @@ struct NodeView: View { )) { NodeView(node: content.value, idPath: idPath + [content.id]) } + case let .slider(value: value, lowerBound: lowerBound, upperBound: upperBound, step: step): + let binding = Binding( + get: { value }, + set: { root.fire(event: .updateSliderValue(value: $0), for: idPath) } + ) + if let step { + Slider(value: binding, in: lowerBound...upperBound, step: step) + } else { + Slider(value: binding, in: lowerBound...upperBound) + } // MARK: Aggregation case let .child(wrapped: wrapped): diff --git a/nuit-core/src/compose/view/widget/mod.rs b/nuit-core/src/compose/view/widget/mod.rs index c2995a8..61b9e2a 100644 --- a/nuit-core/src/compose/view/widget/mod.rs +++ b/nuit-core/src/compose/view/widget/mod.rs @@ -1,9 +1,11 @@ mod button; mod picker; +mod slider; mod text_field; mod text; pub use button::*; pub use picker::*; +pub use slider::*; pub use text_field::*; pub use text::*; diff --git a/nuit-core/src/compose/view/widget/slider.rs b/nuit-core/src/compose/view/widget/slider.rs new file mode 100644 index 0000000..4829b23 --- /dev/null +++ b/nuit-core/src/compose/view/widget/slider.rs @@ -0,0 +1,43 @@ +use std::ops::RangeInclusive; + +use nuit_derive::Bind; + +use crate::{Access, Binding, Context, Event, IdPath, Node, View}; + +/// A control for selecting numeric values from a bounded range. +#[derive(Debug, Clone, Bind)] +pub struct Slider { + value: Binding, + range: RangeInclusive, + step: Option, +} + +impl Slider { + #[must_use] + pub const fn new(value: Binding, range: RangeInclusive, step: Option) -> Self { + Self { value, range, step } + } + + #[must_use] + pub const fn with_default_step(value: Binding, range: RangeInclusive) -> Self { + Self { value, range, step: None } + } +} + +impl View for Slider { + fn fire(&self, event: &Event, event_path: &IdPath, _context: &Context) { + assert!(event_path.is_root()); + if let Event::UpdateSliderValue { value } = event { + self.value.set(*value); + } + } + + fn render(&self, _context: &Context) -> Node { + Node::Slider { + value: self.value.get(), + lower_bound: *self.range.start(), + upper_bound: *self.range.end(), + step: self.step, + } + } +} diff --git a/nuit-core/src/event/event.rs b/nuit-core/src/event/event.rs index 9c8e981..fb846f0 100644 --- a/nuit-core/src/event/event.rs +++ b/nuit-core/src/event/event.rs @@ -13,6 +13,7 @@ pub enum Event { Gesture { gesture: GestureEvent }, UpdateText { content: String }, UpdatePickerSelection { id: Id }, + UpdateSliderValue { value: f64 }, // Lifecycle Appear, diff --git a/nuit-core/src/node/node.rs b/nuit-core/src/node/node.rs index 73ed2cc..ff4341f 100644 --- a/nuit-core/src/node/node.rs +++ b/nuit-core/src/node/node.rs @@ -16,6 +16,7 @@ pub enum Node { TextField { content: String }, Button { label: Box> }, Picker { title: String, selection: Id, content: Box> }, + Slider { value: f64, lower_bound: f64, upper_bound: f64, step: Option }, // Aggregation Child { wrapped: Box> }, diff --git a/nuit-core/src/utils/font/font.rs b/nuit-core/src/utils/font/font.rs index 51d4f9d..d4cb1ea 100644 --- a/nuit-core/src/utils/font/font.rs +++ b/nuit-core/src/utils/font/font.rs @@ -26,8 +26,8 @@ impl Font { pub const FOOTNOTE: Self = Self::with_level(FontLevel::Footnote); #[must_use] - pub fn system(size: impl Into, design: Option, weight: Option) -> Self { - Self::System { size: size.into(), design, weight } + pub fn system(size: impl Into, design: impl Into>, weight: impl Into>) -> Self { + Self::System { size: size.into(), design: design.into(), weight: weight.into() } } #[must_use] @@ -45,3 +45,15 @@ impl Font { Self::System { size: FontSize::level(level), design: None, weight: None } } } + +impl From for Font { + fn from(size: FontSize) -> Self { + Self::with_size(size) + } +} + +impl From for Font { + fn from(level: FontLevel) -> Self { + Self::with_level(level) + } +}