diff --git a/examples/button.rs b/examples/button.rs index df75743..745b961 100644 --- a/examples/button.rs +++ b/examples/button.rs @@ -1,8 +1,8 @@ -use gooey::dynamic::Dynamic; +use gooey::value::Dynamic; use gooey::widgets::Button; -use gooey::{EventLoopError, Run}; +use gooey::Run; -fn main() -> Result<(), EventLoopError> { +fn main() -> gooey::Result { let count = Dynamic::new(0_usize); Button::new(count.map_each(ToString::to_string)) .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) diff --git a/examples/canvas.rs b/examples/canvas.rs index f0cb821..bd37214 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -1,5 +1,5 @@ use gooey::widgets::Canvas; -use gooey::Run; +use gooey::{Run, Tick}; use kludgine::figures::units::Px; use kludgine::figures::{Angle, IntoSigned, Point, Rect, Size}; use kludgine::shapes::Shape; @@ -29,6 +29,6 @@ fn main() -> gooey::Result<()> { None, ) }) - .target_fps(60) + .tick(Tick::redraws_per_second(60)) .run() } diff --git a/examples/counter.rs b/examples/counter.rs index 5477702..3af1d4c 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,27 +1,25 @@ use std::string::ToString; -use gooey::children::Children; -use gooey::dynamic::Dynamic; +use gooey::value::Dynamic; use gooey::widgets::array::Array; use gooey::widgets::{Button, Label}; -use gooey::{EventLoopError, Run}; +use gooey::{widgets, Run}; -fn main() -> Result<(), EventLoopError> { +fn main() -> gooey::Result { let counter = Dynamic::new(0i32); let label = counter.map_each(ToString::to_string); - Array::rows( - Children::new() - .with_widget(Label::new(label)) - .with_widget(Button::new("+").on_click(counter.with_clone(|counter| { - move |_| { - counter.set(counter.get() + 1); - } - }))) - .with_widget(Button::new("-").on_click(counter.with_clone(|counter| { - move |_| { - counter.set(counter.get() - 1); - } - }))), - ) + Array::rows(widgets![ + Label::new(label), + Button::new("+").on_click(counter.with_clone(|counter| { + move |_| { + counter.set(counter.get() + 1); + } + })), + Button::new("-").on_click(counter.with_clone(|counter| { + move |_| { + counter.set(counter.get() - 1); + } + })), + ]) .run() } diff --git a/examples/input.rs b/examples/input.rs index 8111de2..8b042f4 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -1,6 +1,6 @@ use gooey::widgets::Input; -use gooey::{EventLoopError, Run}; +use gooey::Run; -fn main() -> Result<(), EventLoopError> { +fn main() -> gooey::Result { Input::new("Hello").run() } diff --git a/examples/style.rs b/examples/style.rs index abb2eb3..9ee81ee 100644 --- a/examples/style.rs +++ b/examples/style.rs @@ -1,16 +1,16 @@ -use gooey::children::Children; -use gooey::styles::{Styles, TextColor}; -use gooey::widget::Widget; +use gooey::styles::components::TextColor; +use gooey::styles::Styles; +use gooey::widget::{Widget, Widgets}; use gooey::widgets::array::Array; use gooey::widgets::{Button, Style}; use gooey::window::Window; -use gooey::{styles, EventLoopError, Run}; +use gooey::{styles, Run}; use kludgine::Color; -fn main() -> Result<(), EventLoopError> { +fn main() -> gooey::Result { Window::for_widget( Array::rows( - Children::new() + Widgets::new() .with_widget(Button::new("Default")) .with_widget(red_text(Button::new("Styled"))), ) diff --git a/examples/tilemap.rs b/examples/tilemap.rs index ba36298..54f7ad7 100644 --- a/examples/tilemap.rs +++ b/examples/tilemap.rs @@ -1,4 +1,3 @@ -use gooey::dynamic::Dynamic; use gooey::kludgine::app::winit::keyboard::Key; use gooey::kludgine::figures::units::Px; use gooey::kludgine::figures::{Point, Rect, Size}; @@ -6,9 +5,9 @@ use gooey::kludgine::render::Renderer; use gooey::kludgine::shapes::Shape; use gooey::kludgine::tilemap::{Object, ObjectLayer, TileKind, TileMapFocus, Tiles, TILE_SIZE}; use gooey::kludgine::Color; -use gooey::tick::Tick; +use gooey::value::Dynamic; use gooey::widgets::TileMap; -use gooey::{EventLoopError, Run}; +use gooey::{Run, Tick}; const PLAYER_SIZE: Px = Px(16); @@ -28,7 +27,7 @@ const TILES: [TileKind; 64] = { ] }; -fn main() -> Result<(), EventLoopError> { +fn main() -> gooey::Result { let mut characters = ObjectLayer::new(); let myself = characters.push(Player { @@ -43,8 +42,7 @@ fn main() -> Result<(), EventLoopError> { layer: 1, id: myself, }) - .tick(Tick::fps(60, move |elapsed, input| { - // println!("Ticking {input:?}"); + .tick(Tick::times_per_second(60, move |elapsed, input| { let mut direction = Point::new(0., 0.); if input.keys.contains(&Key::ArrowDown) { direction.y += 1.0; diff --git a/src/children.rs b/src/children.rs deleted file mode 100644 index 59330fc..0000000 --- a/src/children.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::ops::{Index, IndexMut}; - -use alot::OrderedLots; - -use crate::widget::{BoxedWidget, MakeWidget}; - -#[derive(Debug, Default)] -#[must_use] -pub struct Children { - ordered: OrderedLots, -} - -impl Children { - pub const fn new() -> Self { - Self { - ordered: OrderedLots::new(), - } - } - - pub fn with_widget(mut self, widget: W) -> Self - where - W: MakeWidget, - { - self.ordered.push(widget.make_widget()); - self - } - - #[must_use] - pub fn len(&self) -> usize { - self.ordered.len() - } - - #[must_use] - pub fn is_empty(&self) -> bool { - self.ordered.is_empty() - } - - #[must_use] - pub fn get(&self, index: usize) -> Option<&BoxedWidget> { - self.ordered.get_by_index(index) - } - - #[must_use] - pub fn iter(&self) -> alot::ordered::Iter<'_, BoxedWidget> { - self.into_iter() - } -} - -impl Index for Children { - type Output = BoxedWidget; - - fn index(&self, index: usize) -> &Self::Output { - &self.ordered[index] - } -} -impl IndexMut for Children { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.ordered[index] - } -} - -impl<'a> IntoIterator for &'a Children { - type IntoIter = alot::ordered::Iter<'a, BoxedWidget>; - type Item = &'a BoxedWidget; - - fn into_iter(self) -> Self::IntoIter { - self.ordered.iter() - } -} diff --git a/src/context.rs b/src/context.rs index fb9e2aa..6143150 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,3 +1,4 @@ +//! Types that provide access to the Gooey runtime. use std::ops::{Deref, DerefMut}; use kludgine::app::winit::event::{ @@ -8,23 +9,43 @@ use kludgine::figures::{IntoSigned, Point, Rect, Size}; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::Kludgine; -use crate::dynamic::Dynamic; use crate::graphics::Graphics; -use crate::styles::{ComponentDefaultvalue, HighlightColor, Styles}; +use crate::styles::components::HighlightColor; +use crate::styles::{ComponentDefaultvalue, Styles}; +use crate::value::Dynamic; use crate::widget::{BoxedWidget, EventHandling, ManagedWidget}; use crate::window::RunningWindow; use crate::ConstraintLimit; +/// A context to an event function. +/// +/// This type is a combination of a reference to the rendering library, +/// [`Kludgine`], and a [`WidgetContext`]. pub struct EventContext<'context, 'window> { + /// The context for the widget receiving the event. pub widget: WidgetContext<'context, 'window>, + /// The rendering library's state. + /// + /// This is useful for accessing the current [scale](Kludgine::scale) or + /// information needed to measure and layout text. pub kludgine: &'context mut Kludgine, } impl<'context, 'window> EventContext<'context, 'window> { - pub fn new(widget: WidgetContext<'context, 'window>, kludgine: &'context mut Kludgine) -> Self { + pub(crate) fn new( + widget: WidgetContext<'context, 'window>, + kludgine: &'context mut Kludgine, + ) -> Self { Self { widget, kludgine } } + /// Returns a new `EventContext` with `widget` being referenced in the + /// contained [`WidgetContext`]. + /// + /// This function is used when one widget contains other widgets, and the + /// parent widget needs to invoke events on a child widget. This is done by + /// creating an `EventContext` pointing to the child and calling the + /// appropriate function to invoke the event. pub fn for_other<'child>( &'child mut self, widget: &'child ManagedWidget, @@ -32,10 +53,14 @@ impl<'context, 'window> EventContext<'context, 'window> { EventContext::new(self.widget.for_other(widget), self.kludgine) } + /// Invokes [`Widget::hit_test()`](crate::widget::Widget::hit_test) on this + /// context's widget and returns the result. pub fn hit_test(&mut self, location: Point) -> bool { self.current_node.lock().hit_test(location, self) } + /// Invokes [`Widget::mouse_down()`](crate::widget::Widget::mouse_down) on + /// this context's widget and returns the result. pub fn mouse_down( &mut self, location: Point, @@ -47,12 +72,16 @@ impl<'context, 'window> EventContext<'context, 'window> { .mouse_down(location, device_id, button, self) } + /// Invokes [`Widget::hit_test()`](crate::widget::Widget::mouse_drag) on + /// this context's widget and returns the result. pub fn mouse_drag(&mut self, location: Point, device_id: DeviceId, button: MouseButton) { self.current_node .lock() .mouse_drag(location, device_id, button, self); } + /// Invokes [`Widget::mouse_up()`](crate::widget::Widget::mouse_up) on this + /// context's widget and returns the result. pub fn mouse_up( &mut self, location: Option>, @@ -64,6 +93,8 @@ impl<'context, 'window> EventContext<'context, 'window> { .mouse_up(location, device_id, button, self); } + /// Invokes [`Widget::keyboard_input()`](crate::widget::Widget::keyboard_input) on this + /// context's widget and returns the result. pub fn keyboard_input( &mut self, device_id: DeviceId, @@ -75,10 +106,14 @@ impl<'context, 'window> EventContext<'context, 'window> { .keyboard_input(device_id, input, is_synthetic, self) } + /// Invokes [`Widget::ime()`](crate::widget::Widget::ime) on this + /// context's widget and returns the result. pub fn ime(&mut self, ime: Ime) -> EventHandling { self.current_node.lock().ime(ime, self) } + /// Invokes [`Widget::mouse_wheel()`](crate::widget::Widget::mouse_wheel) on this + /// context's widget and returns the result. pub fn mouse_wheel( &mut self, device_id: DeviceId, @@ -112,6 +147,46 @@ impl<'context, 'window> EventContext<'context, 'window> { old_hover.lock().unhover(&mut old_hover_context); } } + + pub(crate) fn apply_pending_state(&mut self) { + let active = self.pending_state.active.take(); + if self.current_node.tree.active_widget() != active.as_ref().map(|active| active.id) { + let new = match self.current_node.tree.activate(active.as_ref()) { + Ok(old) => { + if let Some(old) = old { + let mut old_context = self.for_other(&old); + old.lock().deactivate(&mut old_context); + } + true + } + Err(_) => false, + }; + if new { + if let Some(active) = active { + active.lock().activate(self); + } + } + } + + let focus = self.pending_state.focus.take(); + if self.current_node.tree.focused_widget() != focus.as_ref().map(|focus| focus.id) { + let new = match self.current_node.tree.focus(focus.as_ref()) { + Ok(old) => { + if let Some(old) = old { + let mut old_context = self.for_other(&old); + old.lock().blur(&mut old_context); + } + true + } + Err(_) => false, + }; + if new { + if let Some(focus) = focus { + focus.lock().focus(self); + } + } + } + } } impl<'context, 'window> Deref for EventContext<'context, 'window> { @@ -128,8 +203,11 @@ impl<'context, 'window> DerefMut for EventContext<'context, 'window> { } } +/// An owned `T` or an exclusive reference to a `T`. pub enum Exclusive<'a, T> { + /// An exclusive borrow. Borrowed(&'a mut T), + /// An owned instance. Owned(T), } @@ -153,12 +231,19 @@ impl DerefMut for Exclusive<'_, T> { } } +/// A context to a function that is rendering a widget. pub struct GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { + /// The context of the widget being rendered. pub widget: WidgetContext<'context, 'window>, + /// The graphics context clipped and offset to the area of the widget being + /// rendered. Drawing at 0,0 will draw at the top-left pixel of the laid-out + /// widget region. pub graphics: Exclusive<'context, Graphics<'clip, 'gfx, 'pass>>, } impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { + /// Returns a new `GraphicsContext` that allows invoking graphics functions + /// for `widget`. pub fn for_other<'child>( &'child mut self, widget: &'child ManagedWidget, @@ -169,19 +254,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' } } - pub fn measure(&mut self, available_space: Size) -> Size { - self.current_node.lock().measure(available_space, self) - } - - pub fn redraw(&mut self) { - // TODO this should not use clip_rect, because it forces UPx, and once - // we have scrolling, we can have negative offsets of rectangles where - // it's clipped partially. - self.current_node - .note_rendered_rect(self.graphics.clip_rect().into_signed()); - self.current_node.lock().redraw(self); - } - + /// Returns a new graphics context that renders to the `clip` rectangle. pub fn clipped_to(&mut self, clip: Rect) -> GraphicsContext<'_, 'window, '_, 'gfx, 'pass> { GraphicsContext { widget: self.widget.borrowed(), @@ -189,7 +262,11 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' } } - pub fn draw_focus_ring(&mut self, styles: &Styles) { + /// Renders the default focus ring for this widget. + /// + /// To ensure the correct color is used, include [`HighlightColor`] in the + /// styles request. + pub fn draw_focus_ring_using(&mut self, styles: &Styles) { let visible_rect = Rect::from(self.graphics.size() - (UPx(1), UPx(1))); let focus_ring = Shape::stroked_rect( visible_rect, @@ -199,6 +276,28 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' self.graphics .draw_shape(&focus_ring, Point::default(), None, None); } + + /// Renders the default focus ring for this widget. + pub fn draw_focus_ring(&mut self) { + self.draw_focus_ring_using(&self.query_style(&[&HighlightColor])); + } + + /// Invokes [`Widget::measure()`](crate::widget::Widget::measure) on this + /// context's widget and returns the result. + pub fn measure(&mut self, available_space: Size) -> Size { + self.current_node.lock().measure(available_space, self) + } + + /// Invokes [`Widget::redraw()`](crate::widget::Widget::redraw) on this + /// context's widget. + pub fn redraw(&mut self) { + // TODO this should not use clip_rect, because it forces UPx, and once + // we have scrolling, we can have negative offsets of rectangles where + // it's clipped partially. + self.current_node + .note_rendered_rect(self.graphics.clip_rect().into_signed()); + self.current_node.lock().redraw(self); + } } impl<'context, 'window, 'clip, 'gfx, 'pass> Deref @@ -219,9 +318,13 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> DerefMut } } +/// Converts from one context to an [`EventContext`]. pub trait AsEventContext<'window> { + /// Returns this context as an [`EventContext`]. fn as_event_context(&mut self) -> EventContext<'_, 'window>; + /// Pushes a new child widget into the widget hierarchy beneathq the + /// context's widget. #[must_use] fn push_child(&mut self, child: BoxedWidget) -> ManagedWidget { let mut context = self.as_event_context(); @@ -235,6 +338,7 @@ pub trait AsEventContext<'window> { pushed_widget } + /// Removes a widget from the hierarchy. fn remove_child(&mut self, child: &ManagedWidget) { let mut context = self.as_event_context(); context @@ -243,47 +347,6 @@ pub trait AsEventContext<'window> { .remove_child(child, context.current_node); child.lock().unmounted(&mut context.for_other(child)); } - - fn apply_pending_state(&mut self) { - let mut context = self.as_event_context(); - let active = context.pending_state.active.take(); - if context.current_node.tree.active_widget() != active.as_ref().map(|active| active.id) { - let new = match context.current_node.tree.activate(active.as_ref()) { - Ok(old) => { - if let Some(old) = old { - let mut old_context = context.for_other(&old); - old.lock().deactivate(&mut old_context); - } - true - } - Err(_) => false, - }; - if new { - if let Some(active) = active { - active.lock().activate(&mut context); - } - } - } - - let focus = context.pending_state.focus.take(); - if context.current_node.tree.focused_widget() != focus.as_ref().map(|focus| focus.id) { - let new = match context.current_node.tree.focus(focus.as_ref()) { - Ok(old) => { - if let Some(old) = old { - let mut old_context = context.for_other(&old); - old.lock().blur(&mut old_context); - } - true - } - Err(_) => false, - }; - if new { - if let Some(focus) = focus { - focus.lock().focus(&mut context); - } - } - } - } } impl<'window> AsEventContext<'window> for EventContext<'_, 'window> { @@ -298,6 +361,10 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, ' } } +/// A context for a widget. +/// +/// This type provides access to the widget hierarchy from the perspective of a +/// specific widget. pub struct WidgetContext<'context, 'window> { current_node: &'context ManagedWidget, window: &'context mut RunningWindow<'window>, @@ -325,6 +392,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } } + /// Returns a new instance that borrows from `self`. pub fn borrowed(&mut self) -> WidgetContext<'_, 'window> { WidgetContext { current_node: self.current_node, @@ -333,6 +401,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } } + /// Returns a new context representing `widget`. pub fn for_other<'child>( &'child mut self, widget: &'child ManagedWidget, @@ -348,15 +417,21 @@ impl<'context, 'window> WidgetContext<'context, 'window> { self.current_node.parent() } + /// Ensures that this widget will be redrawn when `value` has been updated. pub fn redraw_when_changed(&self, value: &Dynamic) { value.redraw_when_changed(self.window.handle()); } + /// Returns the region that this widget last rendered at. #[must_use] pub fn last_rendered_at(&self) -> Option> { self.current_node.last_rendered_at() } + /// Sets the currently focused widget to this widget. + /// + /// Widget events relating to focus changes are deferred until after the all + /// contexts for the currently firing event are dropped. pub fn focus(&mut self) { self.pending_state.focus = Some(self.current_node.clone()); } @@ -365,6 +440,12 @@ impl<'context, 'window> WidgetContext<'context, 'window> { self.pending_state.focus = None; } + /// Clears focus from this widget, if it is the focused widget. + /// + /// Returns true if this function resulted in the focus being changed. + /// + /// Widget events relating to focus changes are deferred until after the all + /// contexts for the currently firing event are dropped. pub fn blur(&mut self) -> bool { if self.focused() { self.clear_focus(); @@ -374,6 +455,13 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } } + /// Activates this widget, if it is not already active. + /// + /// Returns true if this function resulted in the currently active widget + /// being changed. + /// + /// Widget events relating to activation changes are deferred until after + /// the all contexts for the currently firing event are dropped. pub fn activate(&mut self) -> bool { if self .pending_state @@ -388,6 +476,13 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } } + /// Deactivates this widget, if it is the currently active widget. + /// + /// Returns true if this function resulted in the active widget being + /// changed. + /// + /// Widget events relating to activation changes are deferred until after + /// the all contexts for the currently firing event are dropped. pub fn deactivate(&mut self) -> bool { if self.active() { self.clear_active(); @@ -401,30 +496,48 @@ impl<'context, 'window> WidgetContext<'context, 'window> { self.pending_state.active = None; } + /// Returns true if this widget is currently the active widget. #[must_use] pub fn active(&self) -> bool { self.pending_state.active.as_ref() == Some(self.current_node) } + /// Returns true if this widget is currently hovered. #[must_use] pub fn hovered(&self) -> bool { self.current_node.hovered() } + /// Returns true if this widget is currently focused for user input. #[must_use] pub fn focused(&self) -> bool { self.pending_state.focus.as_ref() == Some(self.current_node) } + /// Returns the widget this context is for. #[must_use] pub const fn widget(&self) -> &ManagedWidget { self.current_node } + /// Attaches `styles` to the widget hierarchy for this widget. + /// + /// Style queries for children will return any values matching this + /// collection. pub fn attach_styles(&self, styles: Styles) { self.current_node.attach_styles(styles); } + /// Queries the widget hierarchy for matching style components. + /// + /// This function traverses up the widget hierarchy looking for the + /// components being requested. The resulting styles will contain the values + /// from the closest matches in the widget hierarchy. + /// + /// For style components to be found, they must have previously been + /// [attached](Self::attach_styles). The [`Style`](crate::widgets::Style) + /// widget is provided as a convenient way to attach styles into the widget + /// hierarchy. #[must_use] pub fn query_style(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { self.current_node.tree.query_style(self.current_node, query) diff --git a/src/graphics.rs b/src/graphics.rs index e03b55f..f993212 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -2,24 +2,36 @@ use std::ops::{Deref, DerefMut}; use kludgine::figures::units::UPx; use kludgine::figures::Rect; +use kludgine::render::Renderer; +use kludgine::ClipGuard; +/// A 2d graphics context pub struct Graphics<'clip, 'gfx, 'pass> { renderer: RenderContext<'clip, 'gfx, 'pass>, } enum RenderContext<'clip, 'gfx, 'pass> { - Renderer(kludgine::render::Renderer<'gfx, 'pass>), - Clipped(kludgine::ClipGuard<'clip, kludgine::render::Renderer<'gfx, 'pass>>), + Renderer(Renderer<'gfx, 'pass>), + Clipped(ClipGuard<'clip, Renderer<'gfx, 'pass>>), } impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { + /// Returns a new graphics context for the given [`Renderer`]. #[must_use] - pub fn new(renderer: kludgine::render::Renderer<'gfx, 'pass>) -> Self { + pub fn new(renderer: Renderer<'gfx, 'pass>) -> Self { Self { renderer: RenderContext::Renderer(renderer), } } + /// Returns a context that has been clipped to `clip`. + /// + /// The new clipping rectangle is interpreted relative to the current + /// clipping rectangle. As a side effect, this function can never expand the + /// clipping rect beyond the current clipping rect. + /// + /// The returned context will report the clipped size, and all drawing + /// operations will be relative to the origin of `clip`. pub fn clipped_to(&mut self, clip: Rect) -> Graphics<'_, 'gfx, 'pass> { Graphics { renderer: RenderContext::Clipped(self.deref_mut().clipped_to(clip)), @@ -28,7 +40,7 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { } impl<'gfx, 'pass> Deref for Graphics<'_, 'gfx, 'pass> { - type Target = kludgine::render::Renderer<'gfx, 'pass>; + type Target = Renderer<'gfx, 'pass>; fn deref(&self) -> &Self::Target { match &self.renderer { diff --git a/src/lib.rs b/src/lib.rs index e770e31..f572076 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,35 +1,38 @@ -#![warn(clippy::pedantic)] -#![allow( - clippy::module_name_repetitions, - clippy::missing_errors_doc, - clippy::missing_panics_doc -)] +//! A reactive, `wgpu`-based Graphical User Interface (GUI) crate for Rust. +#![warn(clippy::pedantic, missing_docs)] +#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)] -pub mod children; pub mod context; -pub mod dynamic; -pub mod graphics; -pub mod names; +mod graphics; +mod names; pub mod styles; -pub mod tick; +mod tick; mod tree; mod utils; +pub mod value; pub mod widget; pub mod widgets; pub mod window; pub use kludgine; -pub use kludgine::app::winit::error::EventLoopError; -pub use kludgine::app::winit::event::ElementState; +use kludgine::app::winit::error::EventLoopError; use kludgine::figures::units::UPx; +pub use names::Name; +pub use self::graphics::Graphics; +pub use self::tick::{InputState, Tick}; + +/// A limit used when measuring a widget. #[derive(Clone, Copy, Eq, PartialEq)] pub enum ConstraintLimit { + /// The widget is expected to occupy a known size. Known(UPx), + /// The widget is expected to resize itself to fit within the size provided. ClippedAfter(UPx), } impl ConstraintLimit { + /// Returns the maximum measurement that will fit the constraint. #[must_use] pub fn max(self) -> UPx { match self { @@ -38,8 +41,59 @@ impl ConstraintLimit { } } -pub type Result = std::result::Result; +/// A result alias that defaults to the result type commonly used throughout +/// this crate. +pub type Result = std::result::Result; +/// A type that can be run as an application. pub trait Run: Sized { - fn run(self) -> Result<(), EventLoopError>; + /// Runs the provided type, returning `Ok(())` upon successful execution and + /// program exit. Note that this function may not ever return on some + /// platforms. + fn run(self) -> crate::Result; +} + +/// Creates a [`Widgets`](crate::widget::Widgets) instance with the given list +/// of widgets. +#[macro_export] +macro_rules! widgets { + () => { + $crate::widget::Widgets::new() + }; + ($($widget:expr),+) => {{ + let mut widgets = $crate::widget::Widgets::with_capacity($crate::count!($($widget),+ ;)); + $(widgets.push($widget);)+ + widgets + }}; + ($($widget:expr),+ ,) => {{ + $crate::widgets!($($widget),+) + }}; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! count { + ($value:expr ;) => { + 1 + }; + ($value:expr , $($remaining:expr),+ ;) => { + 1 + $crate::count!($($remaining),+ ;) + } +} + +/// Creates a [`Styles`](crate::styles::Styles) instance with the given +/// name/component pairs. +#[macro_export] +macro_rules! styles { + () => {{ + $crate::styles::Styles::new() + }}; + ($($component:expr => $value:expr),*) => {{ + let mut styles = $crate::styles::Styles::with_capacity($crate::count!($($value),* ;)); + $(styles.insert(&$component, $value);)* + styles + }}; + ($($component:expr => $value:expr),* ,) => {{ + $crate::styles!($($component => $value),*) + }}; } diff --git a/src/names.rs b/src/names.rs index 3b7093f..cefead6 100644 --- a/src/names.rs +++ b/src/names.rs @@ -5,10 +5,17 @@ use interner::global::{GlobalString, StringPool}; static NAMES: StringPool = StringPool::new(); +/// A smart-string type that is used as a "name" in Gooey. +/// +/// This type ensures that globably only one instance of any unique wrapped +/// string exists. By ensuring all instances of each unique string are the same +/// exact underlying instance, optimizations can be made that avoid string +/// comparisons. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Name(GlobalString); impl Name { + /// Returns a name for the given string. pub fn new<'a>(name: impl Into>) -> Self { Self(NAMES.get(name)) } diff --git a/src/styles.rs b/src/styles.rs index cc97133..9c11d44 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1,3 +1,5 @@ +//! Types for styling widgets. + use std::borrow::Cow; use std::collections::{hash_map, HashMap}; use std::sync::Arc; @@ -5,64 +7,48 @@ use std::sync::Arc; use crate::names::Name; use crate::utils::Lazy; -#[macro_export] -#[doc(hidden)] -macro_rules! count { - ($value:expr ;) => { - 1 - }; - ($value:expr , $($remaining:expr),+ ;) => { - 1 + count!($($remaining),+ ;) - } -} - -#[macro_export] -macro_rules! styles { - () => {{ - $crate::styles::Styles::new() - }}; - ($($component:expr => $value:expr),*) => {{ - let mut styles = $crate::styles::Styles::with_capacity($crate::count!($($value),* ;)); - $(styles.push(&$component, $value);)* - styles - }}; - ($($component:expr => $value:expr),* ,) => {{ - $crate::styles!($($component => $value),*) - }}; -} +pub mod components; +/// A collection of style components organized by their name. #[derive(Clone, Debug, Default)] pub struct Styles(Arc>>); impl Styles { + /// Returns an empty collection. #[must_use] pub fn new() -> Self { Self::default() } + /// Returns a collection with the capacity to hold up to `capacity` elements + /// without reallocating. #[must_use] - pub fn with_capacity(components: usize) -> Self { - Self(Arc::new(HashMap::with_capacity(components))) + pub fn with_capacity(capacity: usize) -> Self { + Self(Arc::new(HashMap::with_capacity(capacity))) } - pub fn push_component(&mut self, name: ComponentName, component: impl Into) { + /// Inserts a [`Component`] with a given name. + pub fn insert_named(&mut self, name: ComponentName, component: impl Into) { Arc::make_mut(&mut self.0) .entry(name.group) .or_default() .insert(name.name, component.into()); } - pub fn push(&mut self, name: &impl NamedComponent, component: impl Into) { + /// Inserts a [`Component`] using then name provided. + pub fn insert(&mut self, name: &impl NamedComponent, component: impl Into) { let name = name.name().into_owned(); - self.push_component(name, component); + self.insert_named(name, component); } + /// Adds a [`Component`] for the name provided and returns self. #[must_use] pub fn with(mut self, name: &impl NamedComponent, component: impl Into) -> Self { - self.push(name, component); + self.insert(name, component); self } + /// Returns the associated component for the given name, if found. #[must_use] pub fn get(&self, component: &Named) -> Option<&Component> where @@ -74,6 +60,8 @@ impl Styles { .and_then(|group| group.get(&name.name)) } + /// Returns the component associated with the given name, or if not found, + /// returns the default value provided by the definition. #[must_use] pub fn get_or_default(&self, component: &Named) -> Named::ComponentType where @@ -93,7 +81,7 @@ impl FromIterator<(ComponentName, Component)> for Styles { let iter = iter.into_iter(); let mut styles = Self::with_capacity(iter.size_hint().0); for (name, component) in iter { - styles.push_component(name, component); + styles.insert_named(name, component); } styles } @@ -113,6 +101,7 @@ impl IntoIterator for Styles { } } +/// An iterator over the owned contents of a [`Styles`] instance. pub struct StylesIntoIter { main: hash_map::IntoIter>, names: Option<(Group, hash_map::IntoIter)>, @@ -136,7 +125,6 @@ impl Iterator for StylesIntoIter { } } -pub type StyleQuery = Vec; use std::any::Any; use std::fmt::Debug; use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -145,12 +133,17 @@ use kludgine::figures::units::{Lp, Px}; use kludgine::figures::ScreenScale; use kludgine::Color; +/// A value of a style component. #[derive(Debug, Clone)] pub enum Component { + /// A color. Color(Color), + /// A single-dimension measurement. Dimension(Dimension), + /// A percentage between 0.0 and 1.0. Percent(f32), - Boxed(BoxedComponent), + /// A custom component type. + Boxed(CustomComponent), } impl From for Component { @@ -221,9 +214,12 @@ impl TryFrom for Lp { } } +/// A 1-dimensional measurement. #[derive(Debug, Clone, Copy)] pub enum Dimension { + /// Physical Pixels Px(Px), + /// Logical Pixels Lp(Lp), } @@ -266,10 +262,12 @@ impl ScreenScale for Dimension { } } +/// A custom component value. #[derive(Debug, Clone)] -pub struct BoxedComponent(Arc); +pub struct CustomComponent(Arc); -impl BoxedComponent { +impl CustomComponent { + /// Wraps an arbitrary value so that it can be used as a [`Component`]. pub fn new(value: T) -> Self where T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, @@ -277,6 +275,8 @@ impl BoxedComponent { Self(Arc::new(value)) } + /// Return the contained value cast as `T`. Returns `None` if `T` does is + /// not the same type that was provided when this component was created. #[must_use] pub fn downcast(&self) -> Option<&T> where @@ -299,10 +299,12 @@ where } } +/// A style component group. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Group(Name); impl Group { + /// Returns a new instance using the group name of `T`. #[must_use] pub fn new() -> Self where @@ -311,6 +313,7 @@ impl Group { Self(T::name()) } + /// Returns true if this instance matches the group name of `T`. #[must_use] pub fn matches(&self) -> bool where @@ -320,10 +323,13 @@ impl Group { } } +/// A type that represents a group of style components. pub trait ComponentGroup { + /// Returns the name of the group. fn name() -> Name; } +/// The Global style components group. pub enum Global {} impl ComponentGroup for Global { @@ -332,13 +338,17 @@ impl ComponentGroup for Global { } } +/// A fully-qualified style component name. #[derive(Clone, Eq, PartialEq, Debug)] pub struct ComponentName { + /// The group name. pub group: Group, + /// The name of the component within the group. pub name: Name, } impl ComponentName { + /// Returns a new instance using `group` and `name`. pub fn new(group: Group, name: impl Into) -> Self { Self { group, @@ -346,6 +356,7 @@ impl ComponentName { } } + /// Returns a new instance using `G` and `name`. pub fn named(name: impl Into) -> Self { Self::new(Group::new::(), name) } @@ -356,17 +367,26 @@ impl From<&'static Lazy> for ComponentName { (**value).clone() } } + +/// A type that represents a named style component. pub trait NamedComponent { + /// Returns the name of the style component. fn name(&self) -> Cow<'_, ComponentName>; } +/// A type that represents a named component with a default value of a specific +/// Rust type. pub trait ComponentDefinition: NamedComponent { + /// The type that will be contained in the [`Component`]. type ComponentType: Into + TryFrom; + /// Returns the default value to use for this component. fn default_value(&self) -> Self::ComponentType; } +/// A type that represents a named component with a default value. pub trait ComponentDefaultvalue: NamedComponent { + /// Returns the default value for this component. fn default_component_value(&self) -> Component; } @@ -390,71 +410,3 @@ impl NamedComponent for Cow<'_, ComponentName> { Cow::Borrowed(self) } } - -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct TextSize; - -impl NamedComponent for TextSize { - fn name(&self) -> Cow<'_, ComponentName> { - Cow::Owned(ComponentName::named::("text_size")) - } -} - -impl ComponentDefinition for TextSize { - type ComponentType = Dimension; - - fn default_value(&self) -> Dimension { - Dimension::Lp(Lp::points(12)) - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct LineHeight; - -impl NamedComponent for LineHeight { - fn name(&self) -> Cow<'_, ComponentName> { - Cow::Owned(ComponentName::named::("line_height")) - } -} - -impl ComponentDefinition for LineHeight { - type ComponentType = Dimension; - - fn default_value(&self) -> Dimension { - Dimension::Lp(Lp::points(14)) - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct TextColor; - -impl NamedComponent for TextColor { - fn name(&self) -> Cow<'_, ComponentName> { - Cow::Owned(ComponentName::named::("text_color")) - } -} - -impl ComponentDefinition for TextColor { - type ComponentType = Color; - - fn default_value(&self) -> Color { - Color::WHITE - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct HighlightColor; - -impl NamedComponent for HighlightColor { - fn name(&self) -> Cow<'_, ComponentName> { - Cow::Owned(ComponentName::named::("highlight_color")) - } -} - -impl ComponentDefinition for HighlightColor { - type ComponentType = Color; - - fn default_value(&self) -> Color { - Color::AQUA - } -} diff --git a/src/styles/components.rs b/src/styles/components.rs new file mode 100644 index 0000000..63cd5c3 --- /dev/null +++ b/src/styles/components.rs @@ -0,0 +1,79 @@ +//! All style components supported by the built-in widgets. +use std::borrow::Cow; + +use kludgine::figures::units::Lp; +use kludgine::Color; + +use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent}; + +/// The [`Dimension`] to use as the size to render text. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct TextSize; + +impl NamedComponent for TextSize { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("text_size")) + } +} + +impl ComponentDefinition for TextSize { + type ComponentType = Dimension; + + fn default_value(&self) -> Dimension { + Dimension::Lp(Lp::points(12)) + } +} + +/// The [`Dimension`] to use to space multiple lines of text. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct LineHeight; + +impl NamedComponent for LineHeight { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("line_height")) + } +} + +impl ComponentDefinition for LineHeight { + type ComponentType = Dimension; + + fn default_value(&self) -> Dimension { + Dimension::Lp(Lp::points(14)) + } +} + +/// The [`Color`] to use when rendering text. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct TextColor; + +impl NamedComponent for TextColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("text_color")) + } +} + +impl ComponentDefinition for TextColor { + type ComponentType = Color; + + fn default_value(&self) -> Color { + Color::WHITE + } +} + +/// A [`Color`] to be used as a highlight color. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct HighlightColor; + +impl NamedComponent for HighlightColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("highlight_color")) + } +} + +impl ComponentDefinition for HighlightColor { + type ComponentType = Color; + + fn default_value(&self) -> Color { + Color::AQUA + } +} diff --git a/src/tick.rs b/src/tick.rs index 98c47f1..3e1579d 100644 --- a/src/tick.rs +++ b/src/tick.rs @@ -6,8 +6,12 @@ use std::time::{Duration, Instant}; use kludgine::app::winit::event::KeyEvent; use kludgine::app::winit::keyboard::Key; -use crate::widget::{EventHandling, HANDLED, UNHANDLED}; +use crate::context::WidgetContext; +use crate::value::Dynamic; +use crate::widget::{EventHandling, HANDLED, IGNORED}; +/// A fixed-rate callback that provides access to tracked input on its +/// associated widget. #[derive(Clone, Debug)] #[must_use] pub struct Tick { @@ -16,12 +20,18 @@ pub struct Tick { } impl Tick { - pub fn rendered(&self) { - self.data.rendered_frame.fetch_add(1, Ordering::AcqRel); + /// Signals that this widget has been redrawn. + pub fn rendered(&self, context: &WidgetContext<'_, '_>) { + context.redraw_when_changed(&self.data.tick_number); self.data.sync.notify_one(); } + /// Processes `input`. + /// + /// If the event matches a key that has been marked as + /// handled, [`HANDLED`] will be returned. Otherwise, [`UNHANDLED`] will be + /// returned, #[must_use] pub fn key_input(&self, input: &KeyEvent) -> EventHandling { let mut state = self.data.state(); @@ -35,13 +45,71 @@ impl Tick { if self.handled_keys.contains(&input.logical_key) { HANDLED } else { - UNHANDLED + IGNORED } } + + /// Returns a new tick that invokes `tick`, aiming to repeat at the given + /// duration. + pub fn new(tick_every: Duration, tick: F) -> Self + where + F: FnMut(Duration, &InputState) + Send + 'static, + { + let now = Instant::now(); + let data = Arc::new(TickData { + state: Mutex::new(TickState { + last_time: now, + next_target: now, + keep_running: true, + frame: 0, + input: InputState::default(), + }), + period: tick_every, + sync: Condvar::new(), + rendered_frame: AtomicUsize::new(0), + tick_number: Dynamic::default(), + }); + + std::thread::spawn({ + let data = data.clone(); + move || tick_loop(&data, tick) + }); + + Self { + data, + handled_keys: HashSet::new(), + } + } + + /// Returns a new tick that invokes `tick` at a target number of times per + /// second. + pub fn times_per_second(times_per_second: u32, tick: F) -> Self + where + F: FnMut(Duration, &InputState) + Send + 'static, + { + Self::new(Duration::from_secs(1) / times_per_second, tick) + } + + /// Returns a new tick that redraws its associated widget at a target rate + /// of `x times_per_second`. + pub fn redraws_per_second(times_per_second: u32) -> Self { + Self::times_per_second(times_per_second, |_, _| {}) + } + + /// Adds the collection of [`Key`]s to the list that are handled, and + /// returns self. + /// + /// The list of keys provided will be prevented from propagating. + pub fn handled_keys(mut self, keys: impl IntoIterator) -> Self { + self.handled_keys.extend(keys); + self + } } +/// The current state of input during the execution of a [`Tick`]. #[derive(Default, Debug)] -pub struct WatchedInput { +pub struct InputState { + /// A collection of all keys currently pressed. pub keys: HashSet, } @@ -51,6 +119,7 @@ struct TickData { period: Duration, sync: Condvar, rendered_frame: AtomicUsize, + tick_number: Dynamic, } impl TickData { @@ -67,55 +136,12 @@ struct TickState { next_target: Instant, keep_running: bool, frame: usize, - input: WatchedInput, -} - -impl Tick { - pub fn new(tick_every: Duration, tick: F) -> Self - where - F: FnMut(Duration, &WatchedInput) + Send + 'static, - { - let now = Instant::now(); - let data = Arc::new(TickData { - state: Mutex::new(TickState { - last_time: now, - next_target: now, - keep_running: true, - frame: 0, - input: WatchedInput::default(), - }), - period: tick_every, - sync: Condvar::new(), - rendered_frame: AtomicUsize::new(0), - }); - - std::thread::spawn({ - let data = data.clone(); - move || tick_loop(&data, tick) - }); - - Self { - data, - handled_keys: HashSet::new(), - } - } - - pub fn fps(frames_per_second: u32, tick: F) -> Self - where - F: FnMut(Duration, &WatchedInput) + Send + 'static, - { - Self::new(Duration::from_secs(1) / frames_per_second, tick) - } - - pub fn handled_keys(mut self, keys: impl IntoIterator) -> Self { - self.handled_keys.extend(keys); - self - } + input: InputState, } fn tick_loop(data: &TickData, mut tick: F) where - F: FnMut(Duration, &WatchedInput), + F: FnMut(Duration, &InputState), { let mut state = data.state(); while state.keep_running { @@ -135,14 +161,15 @@ where .checked_duration_since(state.last_time) .expect("instant never decreases"); state.frame += 1; - // TODO we need a way to batch updates for a context so that during a - // tick, no changed values trigger a redraw until we are done with the - // tick. Otherwise, a frame may start being rendered while we're still - // evaluating the tick since it's in its own thread. + tick(elapsed, &state.input); state.next_target = (state.next_target + data.period).max(now); state.last_time = now; + // Signal that we have a new frame, which will cause the widget to + // redraw. + data.tick_number.map_mut(|tick| *tick += 1); + // Wait for a frame to be rendered. while state.keep_running { let current_frame = data.rendered_frame.load(Ordering::Acquire); diff --git a/src/tree.rs b/src/tree.rs index 91f0ce4..d83eae4 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -203,7 +203,7 @@ impl TreeData { if let Some(styles) = &node.styles { query.retain(|name| { if let Some(component) = styles.get(name) { - resolved.push(name, component.clone()); + resolved.insert(name, component.clone()); false } else { true diff --git a/src/dynamic.rs b/src/value.rs similarity index 52% rename from src/dynamic.rs rename to src/value.rs index e042b56..ef19cd8 100644 --- a/src/dynamic.rs +++ b/src/value.rs @@ -1,21 +1,27 @@ +//! Types for storing and interacting with values in Widgets. + use std::fmt::Debug; use std::panic::AssertUnwindSafe; use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}; use kludgine::app::WindowHandle; +use crate::context::WidgetContext; use crate::window::sealed::WindowCommand; +/// An instance of a value that provides APIs to observe and react to its +/// contents. #[derive(Debug)] pub struct Dynamic(Arc>); impl Dynamic { + /// Creates a new instance wrapping `value`. pub fn new(value: T) -> Self { Self(Arc::new(DynamicData { state: Mutex::new(State { wrapped: GenerationalValue { value, - generation: 0, + generation: Generation::default(), }, callbacks: Vec::new(), windows: Vec::new(), @@ -25,22 +31,30 @@ impl Dynamic { })) } + /// Maps the contents with read-only access. pub fn map_ref(&self, map: impl FnOnce(&T) -> R) -> R { let state = self.state(); map(&state.wrapped.value) } + /// Maps the contents with exclusive access. Before returning from this + /// function, all observers will be notified that the contents have been + /// updated. pub fn map_mut(&self, map: impl FnOnce(&mut T) -> R) -> R { self.0.map_mut(map) } - pub fn for_each(&self, mut map: F) + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + pub fn for_each(&self, mut for_each: F) where F: for<'a> FnMut(&'a T) + Send + 'static, { - self.0.for_each(move |gen| map(&gen.value)); + self.0.for_each(move |gen| for_each(&gen.value)); } + /// Creates a new dynamic value that contains the result of invoking `map` + /// each time this value is changed. pub fn map_each(&self, mut map: F) -> Dynamic where F: for<'a> FnMut(&'a T) -> R + Send + 'static, @@ -49,14 +63,38 @@ impl Dynamic { self.0.map_each(move |gen| map(&gen.value)) } + /// A helper function that invokes `with_clone` with a clone of self. This + /// code may produce slightly more readable code. + /// + /// ```rust + /// let value = gooey::dynamic::Dynamic::new(1); + /// + /// // Using with_clone + /// value.with_clone(|value| { + /// std::thread::spawn(move || { + /// println!("{}", value.get()); + /// }) + /// }); + /// + /// // Using an explicit clone + /// std::thread::spawn({ + /// let value = value.clone(); + /// move || { + /// println!("{}", value.get()); + /// } + /// }); + /// + /// println!("{}", value.get()); + /// ```` pub fn with_clone(&self, with_clone: impl FnOnce(Self) -> R) -> R { with_clone(self.clone()) } - pub fn redraw_when_changed(&self, window: WindowHandle) { + pub(crate) fn redraw_when_changed(&self, window: WindowHandle) { self.0.redraw_when_changed(window); } + /// Returns a clone of the currently contained value. #[must_use] pub fn get(&self) -> T where @@ -65,19 +103,25 @@ impl Dynamic { self.0.get().value } + /// Replaces the contents with `new_value`, returning the previous contents. + /// Before returning from this function, all observers will be notified that + /// the contents have been updated. #[must_use] pub fn replace(&self, new_value: T) -> T { self.0.map_mut(|value| std::mem::replace(value, new_value)) } + /// Stores `new_value` in this dynamic. Before returning from this function, + /// all observers will be notified that the contents have been updated. pub fn set(&self, new_value: T) { let _old = self.replace(new_value); } + /// Returns a new reference-based reader for this dynamic value. #[must_use] - pub fn create_ref_reader(&self) -> DynamicRefReader { + pub fn create_ref_reader(&self) -> DynamicReader { self.state().readers += 1; - DynamicRefReader { + DynamicReader { source: self.0.clone(), read_generation: self.0.state().wrapped.generation, } @@ -87,12 +131,22 @@ impl Dynamic { self.0.state() } + /// Returns the current generation of the value. #[must_use] - pub fn generation(&self) -> usize { + pub fn generation(&self) -> Generation { self.state().wrapped.generation } } +impl Default for Dynamic +where + T: Default, +{ + fn default() -> Self { + Self::new(T::default()) + } +} + impl Clone for Dynamic { fn clone(&self) -> Self { Self(self.0.clone()) @@ -109,7 +163,7 @@ impl Drop for Dynamic { } } -impl From> for DynamicRefReader { +impl From> for DynamicReader { fn from(value: Dynamic) -> Self { value.create_ref_reader() } @@ -149,7 +203,7 @@ impl DynamicData { let mut state = self.state(); let old = { let state = &mut *state; - let generation = state.wrapped.generation.wrapping_add(1); + let generation = state.wrapped.generation.next(); let result = map(&mut state.wrapped.value); state.wrapped.generation = generation; @@ -227,32 +281,45 @@ where } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct GenerationalValue { +struct GenerationalValue { pub value: T, - pub generation: usize, + pub generation: Generation, } +/// A reader that tracks the last generation accessed through this reader. #[derive(Debug)] -pub struct DynamicRefReader { +pub struct DynamicReader { source: Arc>, - read_generation: usize, + read_generation: Generation, } -impl DynamicRefReader { +impl DynamicReader { + /// Maps the contents of the dynamic value and returns the result. + /// + /// This function marks the currently stored value as being read. pub fn map_ref(&mut self, map: impl FnOnce(&T) -> R) -> R { let state = self.source.state(); self.read_generation = state.wrapped.generation; map(&state.wrapped.value) } + /// Returns a clone of the currently contained value. + /// + /// This function marks the currently stored value as being read. #[must_use] - pub fn get(&self) -> T + pub fn get(&mut self) -> T where T: Clone, { - self.source.get().value + let GenerationalValue { value, generation } = self.source.get(); + self.read_generation = generation; + value } + /// Blocks the current thread until the contained value has been updated or + /// there are no remaining writers for the value. + /// + /// Returns true if a newly updated value was discovered. pub fn block_until_updated(&mut self) -> bool { let mut state = self.source.state(); loop { @@ -269,13 +336,9 @@ impl DynamicRefReader { .map_or_else(PoisonError::into_inner, |g| g); } } - - pub fn redraw_if_changed(&mut self, window: WindowHandle) { - self.source.redraw_when_changed(window); - } } -impl Clone for DynamicRefReader { +impl Clone for DynamicReader { fn clone(&self) -> Self { self.source.state().readers += 1; Self { @@ -285,7 +348,7 @@ impl Clone for DynamicRefReader { } } -impl Drop for DynamicRefReader { +impl Drop for DynamicReader { fn drop(&mut self) { let mut state = self.source.state(); state.readers -= 1; @@ -299,3 +362,103 @@ fn disconnecting_reader_from_dynamic() { drop(value); assert!(!ref_reader.block_until_updated()); } + +/// A tag that represents an individual revision of a [`Dynamic`] value. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct Generation(usize); + +impl Generation { + /// Returns the next tag. + #[must_use] + pub fn next(self) -> Self { + Self(self.0.wrapping_add(1)) + } +} + +/// A value that may be either constant or dynamic. +#[derive(Debug)] +pub enum Value { + /// A value that will not ever change externally. + Constant(T), + /// A value that may be updated externally. + Dynamic(Dynamic), +} + +impl Value { + /// Returns a [`Value::Dynamic`] containing `value`. + pub fn dynamic(value: T) -> Self { + Self::Dynamic(Dynamic::new(value)) + } + + /// Maps the current contents to `map` and returns the result. + pub fn map(&mut self, map: impl FnOnce(&T) -> R) -> R { + match self { + Value::Constant(value) => map(value), + Value::Dynamic(dynamic) => dynamic.map_ref(map), + } + } + + /// Maps the current contents with exclusive access and returns the result. + pub fn map_mut(&mut self, map: impl FnOnce(&mut T) -> R) -> R { + match self { + Value::Constant(value) => map(value), + Value::Dynamic(dynamic) => dynamic.map_mut(map), + } + } + + /// Returns a clone of the currently stored value. + pub fn get(&mut self) -> T + where + T: Clone, + { + self.map(Clone::clone) + } + + /// Returns the current generation of the data stored, if the contained + /// value is [`Dynamic`]. + pub fn generation(&self) -> Option { + match self { + Value::Constant(_) => None, + Value::Dynamic(value) => Some(value.generation()), + } + } + + /// Marks the widget for redraw when this value is updated. + /// + /// This function has no effect if the value is constant. + pub fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) { + if let Value::Dynamic(dynamic) = self { + context.redraw_when_changed(dynamic); + } + } +} + +/// A type that can be converted into a [`Value`]. +pub trait IntoValue { + /// Returns this type as a [`Value`]. + fn into_value(self) -> Value; +} + +impl IntoValue for T { + fn into_value(self) -> Value { + Value::Constant(self) + } +} + +impl<'a> IntoValue for &'a str { + fn into_value(self) -> Value { + Value::Constant(self.to_owned()) + } +} + +impl IntoValue for Dynamic { + fn into_value(self) -> Value { + Value::Dynamic(self) + } +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} diff --git a/src/widget.rs b/src/widget.rs index 4a35a91..1858f36 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,61 +1,83 @@ +//! Types for creating reusable widgets (aka components or views). + use std::clone::Clone; use std::fmt::Debug; -use std::ops::ControlFlow; +use std::ops::{ControlFlow, Deref}; use std::panic::UnwindSafe; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; -use kludgine::app::winit::error::EventLoopError; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{Point, Rect, Size}; -use crate::context::{EventContext, GraphicsContext, WidgetContext}; -use crate::dynamic::Dynamic; -use crate::styles::{Component, Group, Styles}; +use crate::context::{EventContext, GraphicsContext}; +use crate::styles::Styles; use crate::tree::{Tree, WidgetId}; use crate::widgets::Style; use crate::window::{RunningWindow, Window, WindowBehavior}; use crate::{ConstraintLimit, Run}; +/// A type that makes up a graphical user interface. +/// +/// This type can go by many names in other UI frameworks: View, Component, +/// Control. pub trait Widget: Send + UnwindSafe + Debug + 'static { + /// Redraw the contents of this widget. fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>); + /// Measure this widget and returns the ideal size based on its contents and + /// the `available_space`. fn measure( &mut self, available_space: Size, context: &mut GraphicsContext<'_, '_, '_, '_, '_>, ) -> Size; + /// The widget has been mounted into a parent widget. #[allow(unused_variables)] fn mounted(&mut self, context: &mut EventContext<'_, '_>) {} + + /// The widget has been removed from its parent widget. #[allow(unused_variables)] fn unmounted(&mut self, context: &mut EventContext<'_, '_>) {} + /// Returns true if this widget should respond to mouse input at `location`. #[allow(unused_variables)] fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { false } + /// The widget is currently has a cursor hovering it at `location`. #[allow(unused_variables)] fn hover(&mut self, location: Point, context: &mut EventContext<'_, '_>) {} + /// The widget is no longer being hovered. #[allow(unused_variables)] fn unhover(&mut self, context: &mut EventContext<'_, '_>) {} + /// The widget has received focus for user input. #[allow(unused_variables)] fn focus(&mut self, context: &mut EventContext<'_, '_>) {} + /// The widget is no longer focused for user input. #[allow(unused_variables)] fn blur(&mut self, context: &mut EventContext<'_, '_>) {} + /// The widget has become the active widget. #[allow(unused_variables)] fn activate(&mut self, context: &mut EventContext<'_, '_>) {} + /// The widget is no longer active. #[allow(unused_variables)] fn deactivate(&mut self, context: &mut EventContext<'_, '_>) {} + /// A mouse button event has occurred at `location`. Returns whether the + /// event has been handled or not. + /// + /// If an event is handled, the widget will receive callbacks for + /// [`mouse_drag`](Self::mouse_drag) and [`mouse_up`](Self::mouse_up). #[allow(unused_variables)] fn mouse_down( &mut self, @@ -64,9 +86,11 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { button: MouseButton, context: &mut EventContext<'_, '_>, ) -> EventHandling { - UNHANDLED + IGNORED } + /// A mouse button is being held down as the cursor is moved across the + /// widget. #[allow(unused_variables)] fn mouse_drag( &mut self, @@ -77,6 +101,7 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { ) { } + /// A mouse button is no longer being pressed. #[allow(unused_variables)] fn mouse_up( &mut self, @@ -87,6 +112,8 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { ) { } + /// A keyboard event has been sent to this widget. Returns whether the event + /// has been handled or not. #[allow(unused_variables)] fn keyboard_input( &mut self, @@ -95,13 +122,18 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { is_synthetic: bool, context: &mut EventContext<'_, '_>, ) -> EventHandling { - UNHANDLED - } - #[allow(unused_variables)] - fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { - UNHANDLED + IGNORED } + /// An input manager event has been sent to this widget. Returns whether the + /// event has been handled or not. + #[allow(unused_variables)] + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + IGNORED + } + + /// A mouse wheel event has been sent to this widget. Returns whether the + /// event has been handled or not. #[allow(unused_variables)] fn mouse_wheel( &mut self, @@ -110,14 +142,17 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { phase: TouchPhase, context: &mut EventContext<'_, '_>, ) -> EventHandling { - UNHANDLED + IGNORED } - #[allow(unused_variables)] - fn query_component(&self, group: Group, name: &str) -> Option { - None - } + // #[allow(unused_variables)] + // fn query_component(&self, group: Group, name: &str) -> Option { + // None + // } + /// Associates `styles` with this widget. + /// + /// This is equivalent to `Style::new(styles, self)`. fn with_styles(self, styles: impl Into) -> Style where Self: Sized, @@ -130,15 +165,18 @@ impl Run for T where T: Widget, { - fn run(self) -> crate::Result<(), EventLoopError> { + fn run(self) -> crate::Result { BoxedWidget::new(self).run() } } +/// A type that can create a widget. pub trait MakeWidget: Sized { + /// Returns a new widget. fn make_widget(self) -> BoxedWidget; - fn run(self) -> Result<(), EventLoopError> { + /// Runs the widget this type creates as an application. + fn run(self) -> crate::Result { self.make_widget().run() } } @@ -152,20 +190,29 @@ where } } +/// A type that represents whether an event has been handled or ignored. pub type EventHandling = ControlFlow; +/// A marker type that represents a handled event. #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub struct EventHandled; #[derive(Debug, Clone, Copy, Eq, PartialEq)] +/// A marker type that represents an ignored event. pub struct EventIgnored; +/// An [`EventHandling`] value that represents a handled event. pub const HANDLED: EventHandling = EventHandling::Break(EventHandled); -pub const UNHANDLED: EventHandling = EventHandling::Continue(EventIgnored); +/// An [`EventHandling`] value that represents an ignored event. +pub const IGNORED: EventHandling = EventHandling::Continue(EventIgnored); + +/// An instance of a [`Widget`]. #[derive(Clone, Debug)] pub struct BoxedWidget(Arc>); impl BoxedWidget { + /// Returns a new instance containing `widget`. pub fn new(widget: W) -> Self where W: Widget, @@ -179,7 +226,7 @@ impl BoxedWidget { } impl Run for BoxedWidget { - fn run(self) -> crate::Result<(), EventLoopError> { + fn run(self) -> crate::Result { Window::::new(self).run() } } @@ -204,84 +251,9 @@ impl WindowBehavior for BoxedWidget { } } -#[derive(Debug)] -pub enum Value { - Constant(T), - Dynamic(Dynamic), -} - -impl Value { - pub fn dynamic(value: T) -> Self { - Self::Dynamic(Dynamic::new(value)) - } - - pub fn constant(value: T) -> Self { - Self::Constant(value) - } - - pub fn map(&mut self, map: impl FnOnce(&T) -> R) -> R { - match self { - Value::Constant(value) => map(value), - Value::Dynamic(dynamic) => dynamic.map_ref(map), - } - } - - pub fn map_mut(&mut self, map: impl FnOnce(&mut T) -> R) -> R { - match self { - Value::Constant(value) => map(value), - Value::Dynamic(dynamic) => dynamic.map_mut(map), - } - } - - pub fn get(&mut self) -> T - where - T: Clone, - { - self.map(Clone::clone) - } - - pub fn generation(&self) -> Option { - match self { - Value::Constant(_) => None, - Value::Dynamic(value) => Some(value.generation()), - } - } - - pub fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) { - if let Value::Dynamic(dynamic) = self { - context.redraw_when_changed(dynamic); - } - } -} - -pub trait IntoValue { - fn into_value(self) -> Value; -} - -impl IntoValue for T { - fn into_value(self) -> Value { - Value::Constant(self) - } -} - -impl<'a> IntoValue for &'a str { - fn into_value(self) -> Value { - Value::Constant(self.to_owned()) - } -} - -impl IntoValue for Dynamic { - fn into_value(self) -> Value { - Value::Dynamic(self) - } -} - -impl IntoValue for Value { - fn into_value(self) -> Value { - self - } -} - +/// A function that can be invoked with a parameter (`T`) and returns `R`. +/// +/// This type is used by widgets to signal various events. pub struct Callback(Box>); impl Debug for Callback { @@ -293,6 +265,8 @@ impl Debug for Callback { } impl Callback { + /// Returns a new instance that calls `function` each time the callback is + /// invoked. pub fn new(function: F) -> Self where F: FnMut(T) -> R + Send + UnwindSafe + 'static, @@ -300,6 +274,7 @@ impl Callback { Self(Box::new(function)) } + /// Invokes the wrapped function and returns the produced value. pub fn invoke(&mut self, value: T) -> R { self.0.invoke(value) } @@ -318,6 +293,7 @@ where } } +/// A [`Widget`] that has been attached to a widget hierarchy. #[derive(Clone)] pub struct ManagedWidget { pub(crate) id: WidgetId, @@ -343,26 +319,31 @@ impl ManagedWidget { self.tree.note_rendered_rect(self.id, rect); } + /// Returns the region that the widget was last rendered at. #[must_use] pub fn last_rendered_at(&self) -> Option> { self.tree.last_rendered_at(self.id) } + /// Returns true if this widget is the currently active widget. #[must_use] pub fn active(&self) -> bool { self.tree.active_widget() == Some(self.id) } + /// Returns true if this widget is currently the hovered widget. #[must_use] pub fn hovered(&self) -> bool { self.tree.hovered_widget() == Some(self.id) } + /// Returns true if this widget is the currently focused widget. #[must_use] pub fn focused(&self) -> bool { self.tree.focused_widget() == Some(self.id) } + /// Returns the parent of this widget. #[must_use] pub fn parent(&self) -> Option { self.tree.parent(self.id).map(|id| self.tree.widget(id)) @@ -384,3 +365,75 @@ impl PartialEq for ManagedWidget { &self.widget == other } } + +/// A list of [`Widget`]s. +#[derive(Debug, Default)] +#[must_use] +pub struct Widgets { + ordered: Vec, +} + +impl Widgets { + /// Returns an empty list. + pub const fn new() -> Self { + Self { + ordered: Vec::new(), + } + } + + /// Returns a list with enough capacity to hold `capacity` widgets without + /// reallocation. + pub fn with_capacity(capacity: usize) -> Self { + Self { + ordered: Vec::with_capacity(capacity), + } + } + + /// Pushes `widget` into the list. + pub fn push(&mut self, widget: W) + where + W: MakeWidget, + { + self.ordered.push(widget.make_widget()); + } + + /// Adds `widget` to self and returns the updated list. + pub fn with_widget(mut self, widget: W) -> Self + where + W: MakeWidget, + { + self.push(widget); + self + } + + /// Returns the number of widgets in this list. + #[must_use] + pub fn len(&self) -> usize { + self.ordered.len() + } + + /// Returns true if there are no widgets in this list. + #[must_use] + pub fn is_empty(&self) -> bool { + self.ordered.is_empty() + } +} + +impl FromIterator for Widgets +where + W: MakeWidget, +{ + fn from_iter>(iter: T) -> Self { + Self { + ordered: iter.into_iter().map(MakeWidget::make_widget).collect(), + } + } +} + +impl Deref for Widgets { + type Target = [BoxedWidget]; + + fn deref(&self) -> &Self::Target { + &self.ordered + } +} diff --git a/src/widgets.rs b/src/widgets.rs index 6332ac7..3975f59 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,3 +1,5 @@ +//! Built-in [`Widget`](crate::widget::Widget) implementations. + pub mod array; mod button; mod canvas; diff --git a/src/widgets/array.rs b/src/widgets/array.rs index a4bac12..7e49e97 100644 --- a/src/widgets/array.rs +++ b/src/widgets/array.rs @@ -1,27 +1,35 @@ +//! A widget that combines an array of [`Widgets`] into one. + use std::ops::Deref; use alot::{LotId, OrderedLots}; use kludgine::figures::units::UPx; use kludgine::figures::{Point, Rect, Size}; -use crate::children::Children; use crate::context::{AsEventContext, EventContext, GraphicsContext}; -use crate::widget::{IntoValue, ManagedWidget, Value, Widget}; +use crate::value::{Generation, IntoValue, Value}; +use crate::widget::{ManagedWidget, Widget, Widgets}; use crate::ConstraintLimit; +/// A widget that displays a collection of [`Widgets`] in a +/// [direction](ArrayDirection). #[derive(Debug)] pub struct Array { + /// The direction to display the children using. pub direction: Value, - pub children: Value, + /// The children widgets that belong to this array. + pub children: Value, layout: Layout, - layout_generation: Option, + layout_generation: Option, + // TODO Refactor synced_children into its own type. synced_children: Vec, } impl Array { + /// Returns a new widget with the given direction and widgets. pub fn new( direction: impl IntoValue, - children: impl IntoValue, + widgets: impl IntoValue, ) -> Self { let mut direction = direction.into_value(); @@ -29,25 +37,27 @@ impl Array { Self { direction, - children: children.into_value(), + children: widgets.into_value(), layout: Layout::new(initial_direction), layout_generation: None, synced_children: Vec::new(), } } - pub fn columns(children: impl IntoValue) -> Self { - Self::new(ArrayDirection::columns(), children) + /// Returns a new instance that displays `widgets` in a series of columns. + pub fn columns(widgets: impl IntoValue) -> Self { + Self::new(ArrayDirection::columns(), widgets) } - pub fn rows(children: impl IntoValue) -> Self { - Self::new(ArrayDirection::rows(), children) + /// Returns a new instance that displays `widgets` in a series of rows. + pub fn rows(widgets: impl IntoValue) -> Self { + Self::new(ArrayDirection::rows(), widgets) } fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { let current_generation = self.children.generation(); if current_generation.map_or_else( - || self.children.map(Children::len) != self.layout.children.len(), + || self.children.map(Widgets::len) != self.layout.children.len(), |gen| Some(gen) != self.layout_generation, ) { self.layout_generation = self.children.generation(); @@ -134,60 +144,99 @@ impl Widget for Array { } } +/// The direction of an [`Array`] widget. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct ArrayDirection { + /// The orientation of the widgets. + pub orientation: ArrayOrientation, + /// If true, the widgets will be laid out in reverse order. + pub reverse: bool, +} + +/// The orientation (Row/Column) of an [`Array`] widget. #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum ArrayDirection { - Row { reverse: bool }, - Column { reverse: bool }, +pub enum ArrayOrientation { + /// The child widgets should be displayed as rows. + Row, + /// The child widgets should be displayed as columns. + Column, } impl ArrayDirection { + /// Display child widgets as columns. #[must_use] pub const fn columns() -> Self { - Self::Column { reverse: false } + Self { + orientation: ArrayOrientation::Column, + reverse: false, + } } + /// Display child widgets as columns in reverse order. #[must_use] pub const fn columns_rev() -> Self { - Self::Column { reverse: true } + Self { + orientation: ArrayOrientation::Column, + reverse: true, + } } + /// Display child widgets as rows. #[must_use] pub const fn rows() -> Self { - Self::Row { reverse: false } + Self { + orientation: ArrayOrientation::Row, + reverse: false, + } } + /// Display child widgets as rows in reverse order. #[must_use] pub const fn rows_rev() -> Self { - Self::Row { reverse: true } - } - - pub fn split_size(&self, s: Size) -> (U, U) { - match self { - Self::Row { .. } => (s.height, s.width), - Self::Column { .. } => (s.width, s.height), + Self { + orientation: ArrayOrientation::Row, + reverse: true, } } - pub fn make_size(&self, measured: U, other: U) -> Size { - match self { - Self::Row { .. } => Size::new(other, measured), - Self::Column { .. } => Size::new(measured, other), + /// Splits a size into its measured and other parts. + pub(crate) fn split_size(self, s: Size) -> (U, U) { + match self.orientation { + ArrayOrientation::Row => (s.height, s.width), + ArrayOrientation::Column => (s.width, s.height), } } - pub fn make_point(&self, measured: U, other: U) -> Point { - match self { - Self::Row { .. } => Point::new(other, measured), - Self::Column { .. } => Point::new(measured, other), + /// Combines split values into a [`Size`]. + pub(crate) fn make_size(self, measured: U, other: U) -> Size { + match self.orientation { + ArrayOrientation::Row => Size::new(other, measured), + ArrayOrientation::Column => Size::new(measured, other), + } + } + + /// Combines split values into a [`Point`]. + pub(crate) fn make_point(self, measured: U, other: U) -> Point { + match self.orientation { + ArrayOrientation::Row => Point::new(other, measured), + ArrayOrientation::Column => Point::new(measured, other), } } } +/// The strategy to use when laying a widget out inside of an [`Array`]. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ArrayDimension { + /// Attempt to lay out the widget based on its contents. FitContent, - Fractional { weight: u8 }, + /// Use a fractional amount of the available space. + Fractional { + /// The weight to apply to this widget when dividing multiple widgets + /// fractionally. + weight: u8, + }, + /// Use an exact measurement for this widget's size. Exact(UPx), } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 10d1387..fa81053 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -11,19 +11,23 @@ use kludgine::Color; use crate::context::{EventContext, GraphicsContext}; use crate::names::Name; -use crate::styles::{ - ComponentDefinition, ComponentGroup, ComponentName, HighlightColor, NamedComponent, TextColor, -}; -use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED}; +use crate::styles::components::{HighlightColor, TextColor}; +use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; +use crate::value::{IntoValue, Value}; +use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; +/// A clickable button. #[derive(Debug)] pub struct Button { + /// The label to display on the button. pub label: Value, + /// The callback that is invoked when the button is clicked. pub on_click: Option>, buttons_pressed: usize, } impl Button { + /// Returns a new button with the provided label. pub fn new(label: impl IntoValue) -> Self { Self { label: label.into_value(), @@ -32,6 +36,9 @@ impl Button { } } + /// Sets the `on_click` callback and returns self. + /// + /// This callback will be invoked each time the button is clicked. #[must_use] pub fn on_click(mut self, callback: F) -> Self where @@ -76,7 +83,7 @@ impl Widget for Button { .draw_shape(&background, Point::default(), None, None); if context.focused() { - context.draw_focus_ring(&styles); + context.draw_focus_ring_using(&styles); } let width = context.graphics.size().width; @@ -198,7 +205,7 @@ impl Widget for Button { } HANDLED } else { - UNHANDLED + IGNORED } } diff --git a/src/widgets/canvas.rs b/src/widgets/canvas.rs index 93b3d80..ebc0b5e 100644 --- a/src/widgets/canvas.rs +++ b/src/widgets/canvas.rs @@ -1,21 +1,24 @@ use std::fmt::Debug; use std::panic::UnwindSafe; -use std::time::{Duration, Instant}; use kludgine::figures::units::UPx; use kludgine::figures::Size; use crate::context::GraphicsContext; +use crate::value::Dynamic; use crate::widget::Widget; +use crate::Tick; +/// A 2d drawable surface. #[must_use] pub struct Canvas { render: Box, - target_frame_duration: Option, - last_frame_time: Option, + tick: Option, + redraw: Dynamic<()>, } impl Canvas { + /// Returns a new canvas that draws its contents by invoking `render`. pub fn new(render: F) -> Self where F: for<'clip, 'gfx, 'pass, 'context, 'window> FnMut( @@ -26,30 +29,24 @@ impl Canvas { { Self { render: Box::new(render), - target_frame_duration: None, - last_frame_time: None, + tick: None, + redraw: Dynamic::new(()), } } - pub fn target_fps(mut self, fps: u16) -> Self { - const ONE_SECOND_NS: u64 = 1_000_000_000; - let frame_duration = ONE_SECOND_NS / u64::from(fps); - self.target_frame_duration = Some(Duration::from_nanos(frame_duration)); + /// Associates a [`Tick`] with this widget and returns self. + pub fn tick(mut self, tick: Tick) -> Self { + self.tick = Some(tick); self } } impl Widget for Canvas { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + context.redraw_when_changed(&self.redraw); self.render.render(context); - - if let Some(target_frame_duration) = self.target_frame_duration { - let now = Instant::now(); - let max_target = now + target_frame_duration; - let next_frame_target = self.last_frame_time.map_or(max_target, |last_frame_time| { - max_target.max(last_frame_time + target_frame_duration) - }); - context.redraw_at(next_frame_target); + if let Some(tick) = &self.tick { + tick.rendered(context); } } diff --git a/src/widgets/input.rs b/src/widgets/input.rs index e8e1111..6a38efd 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -14,24 +14,30 @@ use kludgine::text::TextOrigin; use kludgine::{Color, Kludgine}; use crate::context::{EventContext, WidgetContext}; -use crate::styles::{HighlightColor, LineHeight, Styles, TextColor, TextSize}; +use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize}; +use crate::styles::Styles; use crate::utils::ModifiersExt; -use crate::widget::{EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED}; +use crate::value::{Generation, IntoValue, Value}; +use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500); +/// A text input widget. #[must_use] pub struct Input { + /// The value of this widget. pub text: Value, editor: Option, cursor_state: CursorState, } impl Input { + /// Returns an empty widget. pub fn empty() -> Self { Self::new(String::new()) } + /// Returns a new widget containing `initial_text`. pub fn new(initial_text: impl IntoValue) -> Self { Self { text: initial_text.into_value(), @@ -149,7 +155,7 @@ impl Widget for Input { buffer.shape_until_scroll(context.graphics.font_system()); if context.focused() { - context.draw_focus_ring(&styles); + context.draw_focus_ring_using(&styles); context.set_ime_allowed(true); let line_height = Px::from_float(buffer.metrics().line_height); if let Some(selection) = selection { @@ -310,7 +316,7 @@ impl Widget for Input { context: &mut EventContext<'_, '_>, ) -> EventHandling { if !input.state.is_pressed() { - return UNHANDLED; + return IGNORED; } let styles = context.query_style(&[&TextColor]); @@ -362,7 +368,7 @@ impl Widget for Input { editor.insert_string(&text, None); HANDLED } - (_, _) => UNHANDLED, + (_, _) => IGNORED, }; if handled.is_break() { @@ -400,7 +406,7 @@ impl Widget for Input { struct LiveEditor { editor: Editor, - generation: Option, + generation: Option, } fn cursor_glyph(buffer: &Buffer, cursor: &Cursor) -> Result<(Point, Px), NotVisible> { diff --git a/src/widgets/label.rs b/src/widgets/label.rs index bf40252..186d70a 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -3,30 +3,34 @@ use kludgine::figures::{Point, Size}; use kludgine::text::{Text, TextOrigin}; use crate::context::GraphicsContext; -use crate::styles::TextColor; -use crate::widget::{IntoValue, Value, Widget}; +use crate::styles::components::TextColor; +use crate::value::{IntoValue, Value}; +use crate::widget::Widget; +/// A read-only text widget. #[derive(Debug)] pub struct Label { - pub contents: Value, + /// The contents of the label. + pub text: Value, } impl Label { - pub fn new(contents: impl IntoValue) -> Self { + /// Returns a new label that displays `text`. + pub fn new(text: impl IntoValue) -> Self { Self { - contents: contents.into_value(), + text: text.into_value(), } } } impl Widget for Label { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - self.contents.redraw_when_changed(context); + self.text.redraw_when_changed(context); let center = Point::from(context.graphics.size()) / 2; let styles = context.query_style(&[&TextColor]); let width = context.graphics.size().width; - self.contents.map(|contents| { + self.text.map(|contents| { context.graphics.draw_text( Text::new(contents, styles.get_or_default(&TextColor)) .origin(TextOrigin::Center) @@ -44,7 +48,7 @@ impl Widget for Label { context: &mut GraphicsContext<'_, '_, '_, '_, '_>, ) -> Size { let width = available_space.width.max().try_into().unwrap_or(Px::MAX); - self.contents.map(|contents| { + self.text.map(|contents| { context .graphics .measure_text(Text::from(contents).wrap_at(width)) diff --git a/src/widgets/style.rs b/src/widgets/style.rs index 7ca708a..47ce2da 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -6,6 +6,7 @@ use crate::styles::Styles; use crate::widget::{BoxedWidget, ManagedWidget, Widget}; use crate::ConstraintLimit; +/// A widget that applies a set of [`Styles`] to all contained widgets. #[derive(Debug)] pub struct Style { styles: Styles, @@ -14,6 +15,8 @@ pub struct Style { } impl Style { + /// Returns a new widget that applies `styles` to `child` and any children + /// it may have. pub fn new(styles: impl Into, child: impl Widget) -> Self { Self { styles: styles.into(), diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index bbaf0c4..18192a2 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -1,26 +1,24 @@ use std::fmt::Debug; -use std::panic::UnwindSafe; use kludgine::figures::utils::lossy_f64_to_f32; use crate::context::{EventContext, GraphicsContext}; -use crate::dynamic::Dynamic; use crate::kludgine::app::winit::event::{DeviceId, KeyEvent, MouseScrollDelta, TouchPhase}; -use crate::kludgine::app::winit::keyboard::Key; use crate::kludgine::figures::units::UPx; use crate::kludgine::figures::Size; use crate::kludgine::tilemap; use crate::kludgine::tilemap::TileMapFocus; use crate::tick::Tick; -use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED}; +use crate::value::{Dynamic, IntoValue, Value}; +use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; use crate::ConstraintLimit; +/// A layered tile-based 2d game surface. #[derive(Debug)] #[must_use] pub struct TileMap { layers: Value, focus: Value, - key: Option>, zoom: f32, tick: Option, } @@ -31,32 +29,30 @@ impl TileMap { layers, focus: Value::Constant(TileMapFocus::default()), zoom: 1., - key: None, tick: None, } } + /// Returns a new tilemap that contains dynamic layers. pub fn dynamic(layers: Dynamic) -> Self { Self::construct(Value::Dynamic(layers)) } + /// Returns a new tilemap that renders `layers`. pub fn new(layers: Layers) -> Self { Self::construct(Value::Constant(layers)) } + /// Sets the camera's focus and returns self. + /// + /// The tilemap will ensure that `focus` is centered. + // TODO how do we allow the camera to "lag" for juice effects? pub fn focus_on(mut self, focus: impl IntoValue) -> Self { self.focus = focus.into_value(); self } - pub fn on_key(mut self, key: F) -> Self - where - F: FnMut(Key) -> EventHandling + Send + UnwindSafe + 'static, - { - self.key = Some(Callback::new(key)); - self - } - + /// Associates a [`Tick`] with this widget and returns self. pub fn tick(mut self, tick: Tick) -> Self { self.tick = Some(tick); self @@ -68,15 +64,15 @@ where Layers: tilemap::Layers, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - self.focus.redraw_when_changed(context); - self.layers.redraw_when_changed(context); - let focus = self.focus.get(); self.layers .map(|layers| tilemap::draw(layers, focus, self.zoom, &mut context.graphics)); if let Some(tick) = &self.tick { - tick.rendered(); + tick.rendered(context); + } else { + self.focus.redraw_when_changed(context); + self.layers.redraw_when_changed(context); } } @@ -116,13 +112,7 @@ where if let Some(tick) = &self.tick { tick.key_input(&input)?; } - if !input.state.is_pressed() { - return UNHANDLED; - } - if let Some(on_key) = &mut self.key { - on_key.invoke(input.logical_key.clone())?; - } - UNHANDLED + IGNORED } } diff --git a/src/window.rs b/src/window.rs index 59b0537..9a383cd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,9 +1,11 @@ +//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop +//! window. + use std::cell::RefCell; use std::collections::HashMap; use std::panic::{AssertUnwindSafe, UnwindSafe}; use kludgine::app::winit::dpi::PhysicalPosition; -use kludgine::app::winit::error::EventLoopError; use kludgine::app::winit::event::{ DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; @@ -18,19 +20,24 @@ use crate::context::{EventContext, Exclusive, GraphicsContext, WidgetContext}; use crate::graphics::Graphics; use crate::tree::Tree; use crate::utils::ModifiersExt; -use crate::widget::{BoxedWidget, EventHandling, ManagedWidget, Widget, HANDLED, UNHANDLED}; +use crate::widget::{BoxedWidget, EventHandling, ManagedWidget, Widget, HANDLED, IGNORED}; use crate::window::sealed::WindowCommand; use crate::Run; +/// A currently running Gooey window. pub type RunningWindow<'window> = kludgine::app::Window<'window, WindowCommand>; + +/// The attributes of a Gooey window. pub type WindowAttributes = kludgine::app::WindowAttributes; +/// A Gooey window that is not yet running. #[must_use] pub struct Window where Behavior: WindowBehavior, { context: Behavior::Context, + /// The attributes of this window. pub attributes: WindowAttributes, } @@ -46,6 +53,7 @@ where } impl Window { + /// Returns a new instance using `widget` as its contents. pub fn for_widget(widget: W) -> Self where W: Widget, @@ -58,6 +66,8 @@ impl Window where Behavior: WindowBehavior, { + /// Returns a new instance using `context` to initialize the window upon + /// opening. pub fn new(context: Behavior::Context) -> Self { Self { attributes: WindowAttributes { @@ -73,36 +83,45 @@ impl Run for Window where Behavior: WindowBehavior, { - fn run(self) -> crate::Result<(), EventLoopError> { + fn run(self) -> crate::Result { GooeyWindow::::run_with(AssertUnwindSafe(( self.context, - RefCell::new(WindowSettings { + RefCell::new(sealed::WindowSettings { attributes: Some(self.attributes), }), ))) } } +/// The behavior of a Gooey window. pub trait WindowBehavior: Sized + 'static { + /// The type that is provided when initializing this window. type Context: UnwindSafe + Send + 'static; + /// Return a new instance of this behavior using `context`. fn initialize(window: &mut RunningWindow<'_>, context: Self::Context) -> Self; + /// Create the window's root widget. This function is only invoked once. fn make_root(&mut self) -> BoxedWidget; + /// The window has been requested to close. If this function returns true, + /// the window will be closed. Returning false prevents the window from + /// closing. #[allow(unused_variables)] fn close_requested(&self, window: &mut RunningWindow<'_>) -> bool { true } - fn run() -> Result<(), EventLoopError> + /// Runs this behavior as an application. + fn run() -> crate::Result where Self::Context: Default, { Self::run_with(::default()) } - fn run_with(context: Self::Context) -> Result<(), EventLoopError> { + /// Runs this behavior as an application, initialized with `context`. + fn run_with(context: Self::Context) -> crate::Result { Window::::new(context).run() } } @@ -130,7 +149,7 @@ impl kludgine::app::WindowBehavior for GooeyWindow where T: WindowBehavior, { - type Context = AssertUnwindSafe<(T::Context, RefCell)>; + type Context = AssertUnwindSafe<(T::Context, RefCell)>; fn initialize( mut window: RunningWindow<'_>, @@ -415,16 +434,12 @@ fn recursively_handle_event( ) -> Option { match each_widget(context) { HANDLED => Some(context.widget().clone()), - UNHANDLED => context.parent().and_then(|parent| { + IGNORED => context.parent().and_then(|parent| { recursively_handle_event(&mut context.for_other(&parent), each_widget) }), } } -pub struct WindowSettings { - attributes: Option, -} - #[derive(Default)] struct MouseState { location: Option>, @@ -433,6 +448,12 @@ struct MouseState { } pub(crate) mod sealed { + use crate::window::WindowAttributes; + + pub struct WindowSettings { + pub attributes: Option, + } + pub enum WindowCommand { Redraw, // RequestClose,