diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs new file mode 100644 index 0000000..6137a8d --- /dev/null +++ b/examples/custom-widgets.rs @@ -0,0 +1,120 @@ +//! This example shows two approaches to writing custom widgets: implementing +//! traits or using the [`Custom`] widget with callbacks. + +use gooey::value::Dynamic; +use gooey::widget::{MakeWidget, Widget, HANDLED}; +use gooey::widgets::Custom; +use gooey::Run; +use kludgine::figures::units::{Lp, UPx}; +use kludgine::figures::{ScreenScale, Size}; +use kludgine::Color; + +fn main() -> gooey::Result { + "Inline Widgets" + .and(callback_widget()) + .into_rows() + .and( + "impl MakeWidget" + .and(ToggleMakeWidget::default()) + .into_rows(), + ) + .and("impl Widget".and(impl_widget()).into_rows()) + .into_columns() + .centered() + .expand() + .run() +} + +/// This function returns a [`Custom`] widget that implements its functionality +/// using callbacks. +/// +/// This approach was added to make it easy to create one-off widgets in a +/// hierarchy to handle events or other purpose-built functions. +fn callback_widget() -> impl MakeWidget { + // This implementation and the impl `Widget` implementation both use the + // same Dynamic value setup. + let toggle = Toggle::default(); + + Custom::empty() + .background_color(toggle.color) + .on_hit_test(|_, _| true) + .on_mouse_down(move |_, _, _, _| { + toggle.value.toggle(); + HANDLED + }) + .height(Lp::inches(1)) +} + +/// A second approach is to implement [`MakeWidget`] for a type. This allows any +/// type to be used when composing your UI that know how to create a widget. +/// +/// This enables using callback-based widgets (or any other combination of +/// widgets) in a reusable fashion. +#[derive(Default)] +struct ToggleMakeWidget(Toggle); + +impl MakeWidget for ToggleMakeWidget { + fn make_widget(self) -> gooey::widget::WidgetInstance { + // In a real code base, the contents of callback_widget() would go here + callback_widget().make_widget() + } +} + +/// This function returns [`Toggle`] using its [`Widget`] implementation. +/// +/// This is the lowest-level way to implement a Widget, but it also provides the +/// most power and flexibility. +fn impl_widget() -> impl MakeWidget { + Toggle::default() +} + +#[derive(Debug)] +struct Toggle { + value: Dynamic, + color: Dynamic, +} + +impl Default for Toggle { + fn default() -> Self { + let value = Dynamic::default(); + let color = value.map_each(|on| if *on { Color::RED } else { Color::BLUE }); + Self { value, color } + } +} + +impl Widget for Toggle { + fn redraw(&mut self, context: &mut gooey::context::GraphicsContext<'_, '_, '_, '_, '_>) { + context.fill(self.color.get_tracking_refresh(context)); + } + + fn layout( + &mut self, + available_space: Size, + context: &mut gooey::context::LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + Size::new( + available_space.width.min(), + Lp::inches(1).into_upx(context.gfx.scale()), + ) + } + + fn hit_test( + &mut self, + _location: kludgine::figures::Point, + _context: &mut gooey::context::EventContext<'_, '_>, + ) -> bool { + true + } + + fn mouse_down( + &mut self, + _location: kludgine::figures::Point, + _device_id: kludgine::app::winit::event::DeviceId, + _button: kludgine::app::winit::event::MouseButton, + _context: &mut gooey::context::EventContext<'_, '_>, + ) -> gooey::widget::EventHandling { + self.value.toggle(); + + HANDLED + } +} diff --git a/src/context.rs b/src/context.rs index 87f0d59..c1a672c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -10,7 +10,7 @@ use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::figures::units::{Lp, Px, UPx}; -use kludgine::figures::{IntoSigned, Point, Px2D, Rect, ScreenScale, Size, Zero}; +use kludgine::figures::{IntoSigned, Point, Px2D, Rect, Round, ScreenScale, Size, Zero}; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::{Color, Kludgine}; @@ -610,7 +610,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' ); let background = self.get(&WidgetBackground); - self.gfx.fill(background); + self.fill(background); self.apply_current_font_settings(); @@ -707,7 +707,8 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'cl .clone() .lock() .as_widget() - .layout(available_space, self); + .layout(available_space, self) + .map(Round::ceil); if self.persist_layout { self.graphics .current_node diff --git a/src/lib.rs b/src/lib.rs index 8a4c6c4..a4c81e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,16 @@ pub enum ConstraintLimit { } impl ConstraintLimit { + /// Returns `UPx::ZERO` when sizing to fit, otherwise it returns the size + /// being filled. + #[must_use] + pub fn min(self) -> UPx { + match self { + ConstraintLimit::Fill(v) => v, + ConstraintLimit::SizeToFit(_) => UPx::ZERO, + } + } + /// Returns the maximum measurement that will fit the constraint. #[must_use] pub fn max(self) -> UPx { diff --git a/src/styles/components.rs b/src/styles/components.rs index eb69642..13d6968 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -20,6 +20,7 @@ use crate::styles::{Dimension, FocusableWidgets, FontFamilyList, VisualOrder}; /// use gooey::styles::Dimension; /// use gooey::styles::components::{SurfaceColor, TextColor}; /// use gooey::kludgine::Color; +/// use gooey::kludgine::figures::Zero; /// /// define_components! { /// GroupName { diff --git a/src/tree.rs b/src/tree.rs index afdf43f..5b8212d 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -132,8 +132,8 @@ impl Tree { let node = &mut data.nodes[parent]; if let Some(cached_layout) = &node.last_layout_query { - if constraints.width.max() < cached_layout.constraints.width.max() - && constraints.height.max() < cached_layout.constraints.height.max() + if constraints.width.max() <= cached_layout.constraints.width.max() + && constraints.height.max() <= cached_layout.constraints.height.max() { return Some(cached_layout.size); } diff --git a/src/value.rs b/src/value.rs index 52bfe47..4d81840 100644 --- a/src/value.rs +++ b/src/value.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use std::fmt::{Debug, Display}; use std::future::Future; -use std::ops::{Deref, DerefMut}; +use std::ops::{Deref, DerefMut, Not}; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; use std::task::{Poll, Waker}; @@ -134,6 +134,19 @@ impl Dynamic { self.0.map_mut(|value, _| map(value)).expect("deadlocked") } + /// Updates the value to the result of invoking [`Not`] on the current + /// value. This function returns the new value. + #[allow(clippy::must_use_candidate)] + pub fn toggle(&self) -> T + where + T: Not + Clone, + { + self.map_mut(|value| { + *value = !value.clone(); + value.clone() + }) + } + /// Returns a new dynamic that is updated using `U::from(T.clone())` each /// time `self` is updated. #[must_use] diff --git a/src/widget.rs b/src/widget.rs index 13a982c..5c74c6b 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -42,11 +42,14 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { /// Layout this widget and returns the ideal size based on its contents and /// the `available_space`. + #[allow(unused_variables)] fn layout( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> Size; + ) -> Size { + available_space.map(ConstraintLimit::min) + } /// Return true if this widget should expand to fill the window when it is /// the root widget. @@ -250,7 +253,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> WrappedLayout { - let adjusted_space = self.adjust_child_constraint(available_space, context); + let adjusted_space = self.adjust_child_constraints(available_space, context); let child = self.child_mut().mounted(&mut context.as_event_context()); let size = context .for_other(&child) @@ -263,7 +266,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { /// Returns the adjusted contraints to use when laying out the child. #[allow(unused_variables)] #[must_use] - fn adjust_child_constraint( + fn adjust_child_constraints( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, @@ -430,7 +433,7 @@ where fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { let background_color = self.background_color(context); if let Some(color) = background_color { - context.gfx.fill(color); + context.fill(color); } self.redraw_background(context); diff --git a/src/widgets.rs b/src/widgets.rs index 2bd7c4d..079bbcf 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -5,6 +5,7 @@ pub mod button; mod canvas; pub mod checkbox; pub mod container; +mod custom; mod expand; pub mod input; pub mod label; @@ -24,6 +25,7 @@ pub use button::Button; pub use canvas::Canvas; pub use checkbox::Checkbox; pub use container::Container; +pub use custom::Custom; pub use expand::Expand; pub use input::Input; pub use label::Label; diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 7b28c4d..27ea465 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -188,7 +188,7 @@ impl WrapperWidget for Container { }) } - fn adjust_child_constraint( + fn adjust_child_constraints( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, diff --git a/src/widgets/custom.rs b/src/widgets/custom.rs new file mode 100644 index 0000000..baf5057 --- /dev/null +++ b/src/widgets/custom.rs @@ -0,0 +1,779 @@ +use std::fmt::Debug; +use std::panic::UnwindSafe; + +use kludgine::app::winit::event::{ + DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, +}; +use kludgine::figures::units::Px; +use kludgine::figures::{Point, Size}; +use kludgine::Color; + +use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; +use crate::value::{IntoValue, Value}; +use crate::widget::{EventHandling, MakeWidget, WidgetRef, WrappedLayout, WrapperWidget, IGNORED}; +use crate::widgets::Space; +use crate::ConstraintLimit; + +/// A callback-based custom widget. +/// +/// This type can be used to create inline widgets without defining a new type +/// and implementing [`Widget`]/[`WrapperWidget`] for it. +#[must_use] +pub struct Custom { + child: WidgetRef, + redraw_foreground: Option>, + redraw_background: Option>, + mounted: Option>, + unmounted: Option>, + background: Option>, + unhover: Option>, + focus: Option>, + blur: Option>, + activate: Option>, + deactivate: Option>, + accept_focus: Option>>, + adjust_child: Option>, + position_child: Option>, + hit_test: Option, bool>>>, + hover: Option>>>, + mouse_down: + Option, DeviceId, MouseButton, EventHandling>>>, + mouse_drag: Option, DeviceId, MouseButton>>>, + mouse_up: Option>, + ime: Option>>, + keyboard_input: Option>>, + mouse_wheel: + Option>>, +} + +impl Debug for Custom { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Custom") + .field("child", &self.child) + .finish_non_exhaustive() + } +} + +impl Default for Custom { + fn default() -> Self { + Self::empty() + } +} + +impl Custom { + /// Returns a custom widget that has no child. + pub fn empty() -> Self { + Self::new(Space::clear()) + } + + /// Returns a custom widget that contains `child`. + pub fn new(child: impl MakeWidget) -> Self { + Self { + child: WidgetRef::new(child), + redraw_background: None, + redraw_foreground: None, + background: None, + mounted: None, + unmounted: None, + unhover: None, + focus: None, + blur: None, + activate: None, + deactivate: None, + accept_focus: None, + adjust_child: None, + position_child: None, + hit_test: None, + hover: None, + mouse_down: None, + mouse_drag: None, + mouse_up: None, + ime: None, + keyboard_input: None, + mouse_wheel: None, + } + } + + /// Sets the background color of this widget to `color` and returns self. + /// + /// If the color is set to a non-transparent value, it will be filled before + /// any of the redraw callbacks are invoked. + /// + /// This value coresponds to [`WrapperWidget::background_color`]. + pub fn background_color(mut self, color: impl IntoValue) -> Self { + self.background = Some(color.into_value()); + self + } + + /// Sets `redraw` as the callback to invoke when redrawing this control. + /// + /// If this control contains a child, its redraw function will be invoked + /// after `redraw` is invoked. Use [`Self::on_redraw_after_child()`] to draw + /// after the child widget. + /// + /// This callback corresponds to [`WrapperWidget::redraw_background`]. + pub fn on_redraw(mut self, redraw: Redraw) -> Self + where + Redraw: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + ), + { + self.redraw_background = Some(Box::new(redraw)); + self + } + + /// Sets `redraw` as the callback to invoke when redrawing this control, + /// after the child control has been redrawn. + /// + /// If this control contains a child, its redraw function will be invoked + /// before `redraw` is invoked. Use [`Self::on_redraw()`] to draw before the + /// child widget. + /// + /// `redraw` will be invoked regardless of whether a child is present. + /// + /// This callback corresponds to [`WrapperWidget::redraw_foreground`]. + pub fn on_redraw_after_child(mut self, redraw: Redraw) -> Self + where + Redraw: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + ), + { + self.redraw_foreground = Some(Box::new(redraw)); + self + } + + /// Sets `mounted` to be invoked when this widget is mounted into a parent. + /// + /// This callback corresponds to [`WrapperWidget::mounted`]. + pub fn on_mounted(mut self, mounted: Mounted) -> Self + where + Mounted: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.mounted = Some(Box::new(mounted)); + self + } + + /// Sets `unmounted` to be invoked when this widget is unmounted from its + /// parent. + /// + /// This callback corresponds to [`WrapperWidget::unmounted`]. + pub fn on_unmounted(mut self, mounted: Mounted) -> Self + where + Mounted: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.unmounted = Some(Box::new(mounted)); + self + } + + /// Invokes `unhovered` when the mouse cursor leaves the widget's boundary. + /// + /// This callback corresponds to [`WrapperWidget::unhover`]. + pub fn on_unhover(mut self, unhovered: Unhover) -> Self + where + Unhover: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.unhover = Some(Box::new(unhovered)); + self + } + + /// Invokes `focus` when the widget receives input focus. + /// + /// This callback corresponds to [`WrapperWidget::focus`]. + pub fn on_focus(mut self, focus: Focused) -> Self + where + Focused: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.focus = Some(Box::new(focus)); + self + } + + /// Invokes `blur` when the widget loses input focus. + /// + /// This callback corresponds to [`WrapperWidget::blur`]. + pub fn on_blur(mut self, blur: Blur) -> Self + where + Blur: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.blur = Some(Box::new(blur)); + self + } + + /// Invokes `activated` when this widget becomes the active widget. + /// + /// This callback corresponds to [`WrapperWidget::activate`]. + pub fn on_activate(mut self, activated: Activated) -> Self + where + Activated: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.activate = Some(Box::new(activated)); + self + } + + /// Invokes `deactivated` when this widget no longer is the active widget. + /// + /// This callback corresponds to [`WrapperWidget::deactivate`]. + pub fn on_deactivate(mut self, deactivated: Deactivated) -> Self + where + Deactivated: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + { + self.deactivate = Some(Box::new(deactivated)); + self + } + + /// Invokes `accept` when this widget is set to receive input focus. If this + /// function returns true, this widget will become the focused widget. + /// + /// This callback corresponds to [`WrapperWidget::accept_focus`]. + pub fn on_accept_focus(mut self, accept: AcceptFocus) -> Self + where + AcceptFocus: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>) -> bool, + { + self.accept_focus = Some(Box::new(accept)); + self + } + + /// Invokes `adjust_child_constraints` before measuring the child widget. + /// The returned constraints will be passed along to the child in its layout + /// function. + /// + /// This callback corresponds to [`WrapperWidget::adjust_child_constraints`]. + pub fn on_adjust_child_constraints( + mut self, + adjust_child_constraints: AdjustChildConstraints, + ) -> Self + where + AdjustChildConstraints: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + Size, + &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + ) -> Size, + { + self.adjust_child = Some(Box::new(adjust_child_constraints)); + self + } + + /// Invokes `position_child` to determine the position of a measured child. + /// + /// This callback corresponds to [`WrapperWidget::position_child`]. + pub fn on_position_child(mut self, position_child: PositionChild) -> Self + where + PositionChild: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + Size, + Size, + &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + ) -> WrappedLayout, + { + self.position_child = Some(Box::new(position_child)); + self + } + + /// Invokes `hit_test` when determining if a location should be considered + /// interacting with this widget. + /// + /// This callback corresponds to [`WrapperWidget::hit_test`]. + pub fn on_hit_test(mut self, hit_test: HitTest) -> Self + where + HitTest: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(Point, &mut EventContext<'context, 'window>) -> bool, + { + self.hit_test = Some(Box::new(hit_test)); + self + } + + /// Invokes `hover` when a mouse cursor is above this widget. + /// + /// This callback corresponds to [`WrapperWidget::hover`]. + pub fn on_hover(mut self, hover: Hover) -> Self + where + Hover: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(Point, &mut EventContext<'context, 'window>), + { + self.hover = Some(Box::new(hover)); + self + } + + /// Invokes `mouse_down` when a mouse button is pushed on a location where + /// [`Self::on_hit_test`] returned true. + /// + /// Returning [`HANDLED`](crate::widget::HANDLED) will set this widget as + /// the handler for the [`DeviceId`] and [`MouseButton`]. Future mouse + /// events for the same device and button will be sent to this widget's + /// [`Self::on_mouse_drag`] and [`Self::on_mouse_up`] callbacks. + /// + /// This callback corresponds to [`WrapperWidget::mouse_down`]. + pub fn on_mouse_down(mut self, mouse_down: MouseDown) -> Self + where + MouseDown: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut( + Point, + DeviceId, + MouseButton, + &mut EventContext<'context, 'window>, + ) -> EventHandling, + { + self.mouse_down = Some(Box::new(mouse_down)); + self + } + + /// Invokes `mouse_drag` when the mouse cursor moves while a tracked button + /// is presed. + /// + /// This callback corresponds to [`WrapperWidget::mouse_drag`]. + pub fn on_mouse_drag(mut self, mouse_drag: MouseDrag) -> Self + where + MouseDrag: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut( + Point, + DeviceId, + MouseButton, + &mut EventContext<'context, 'window>, + ), + { + self.mouse_drag = Some(Box::new(mouse_drag)); + self + } + + /// Invokes `mouse_up` when a tracked mouse button is released. + /// + /// This callback corresponds to [`WrapperWidget::mouse_up`]. + pub fn on_mouse_up(mut self, mouse_up: MouseUp) -> Self + where + MouseUp: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut( + Option>, + DeviceId, + MouseButton, + &mut EventContext<'context, 'window>, + ), + { + self.mouse_up = Some(Box::new(mouse_up)); + self + } + + /// Invokes `ime` when an input manager event occurs. + /// + /// This callback corresponds to [`WrapperWidget::ime`]. + pub fn on_ime(mut self, ime: OnIme) -> Self + where + OnIme: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(Ime, &mut EventContext<'context, 'window>) -> EventHandling, + { + self.ime = Some(Box::new(ime)); + self + } + + /// Invokes `keyboard_input` when a keyboard event occurs. + /// + /// This callback corresponds to [`WrapperWidget::keyboard_input`]. + pub fn on_keyboard_input(mut self, keyboard_input: KeyboardInput) -> Self + where + KeyboardInput: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut( + DeviceId, + KeyEvent, + bool, + &mut EventContext<'context, 'window>, + ) -> EventHandling, + { + self.keyboard_input = Some(Box::new(keyboard_input)); + self + } + + /// Invokes `mouse_wheel` when a mouse wheel event occurs. + /// + /// This callback corresponds to [`WrapperWidget::mouse_wheel`]. + pub fn mouse_wheel(mut self, mouse_wheel: MouseWheel) -> Self + where + MouseWheel: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut( + DeviceId, + MouseScrollDelta, + TouchPhase, + &mut EventContext<'context, 'window>, + ) -> EventHandling, + { + self.mouse_wheel = Some(Box::new(mouse_wheel)); + self + } +} + +impl WrapperWidget for Custom { + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.child + } + + fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + if let Some(redraw) = &mut self.redraw_background { + redraw.invoke(context); + } + } + + fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + if let Some(redraw) = &mut self.redraw_foreground { + redraw.invoke(context); + } + } + + fn adjust_child_constraints( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + if let Some(adjust_child) = &mut self.adjust_child { + adjust_child.invoke(available_space, context) + } else { + available_space + } + } + + fn position_child( + &mut self, + size: Size, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> WrappedLayout { + if let Some(position_child) = &mut self.position_child { + position_child.invoke(size, available_space, context) + } else { + Size::new( + available_space + .width + .fit_measured(size.width, context.gfx.scale()), + available_space + .height + .fit_measured(size.height, context.gfx.scale()), + ) + .into() + } + } + + fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { + self.background.as_ref().map(|bg| bg.get_tracked(context)) + } + + fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(mounted) = &mut self.mounted { + mounted.invoke(context); + } + } + + fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(unmounted) = &mut self.unmounted { + unmounted.invoke(context); + } + } + + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + if let Some(hit_test) = &mut self.hit_test { + hit_test.invoke(location, context) + } else { + false + } + } + + fn hover(&mut self, location: Point, context: &mut EventContext<'_, '_>) { + if let Some(hover) = &mut self.hover { + hover.invoke(location, context); + } + } + + fn unhover(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(unhover) = &mut self.unhover { + unhover.invoke(context); + } + } + + fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + if let Some(accept_focus) = &mut self.accept_focus { + accept_focus.invoke(context) + } else { + false + } + } + + fn focus(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(focus) = &mut self.focus { + focus.invoke(context); + } + } + + fn blur(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(blur) = &mut self.blur { + blur.invoke(context); + } + } + + fn activate(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(activate) = &mut self.activate { + activate.invoke(context); + } + } + + fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { + if let Some(deactivate) = &mut self.deactivate { + deactivate.invoke(context); + } + } + + fn mouse_down( + &mut self, + location: Point, + device_id: DeviceId, + button: MouseButton, + context: &mut EventContext<'_, '_>, + ) -> EventHandling { + if let Some(mouse_down) = &mut self.mouse_down { + mouse_down.invoke(location, device_id, button, context) + } else { + IGNORED + } + } + + fn mouse_drag( + &mut self, + location: Point, + device_id: DeviceId, + button: MouseButton, + context: &mut EventContext<'_, '_>, + ) { + if let Some(mouse_drag) = &mut self.mouse_drag { + mouse_drag.invoke(location, device_id, button, context); + } + } + + fn mouse_up( + &mut self, + location: Option>, + device_id: DeviceId, + button: MouseButton, + context: &mut EventContext<'_, '_>, + ) { + if let Some(mouse_up) = &mut self.mouse_up { + mouse_up.invoke(location, device_id, button, context); + } + } + + fn keyboard_input( + &mut self, + device_id: DeviceId, + input: KeyEvent, + is_synthetic: bool, + context: &mut EventContext<'_, '_>, + ) -> EventHandling { + if let Some(keyboard_input) = &mut self.keyboard_input { + keyboard_input.invoke(device_id, input, is_synthetic, context) + } else { + IGNORED + } + } + + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + if let Some(f) = &mut self.ime { + f.invoke(ime, context) + } else { + IGNORED + } + } + + fn mouse_wheel( + &mut self, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + context: &mut EventContext<'_, '_>, + ) -> EventHandling { + if let Some(mouse_wheel) = &mut self.mouse_wheel { + mouse_wheel.invoke(device_id, delta, phase, context) + } else { + IGNORED + } + } +} + +trait RedrawFunc: Send + UnwindSafe { + fn invoke(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>); +} + +impl RedrawFunc for Func +where + Func: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + ), +{ + fn invoke(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + self(context); + } +} + +trait AdjustChildConstraintsFunc: Send + UnwindSafe { + fn invoke( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size; +} + +impl AdjustChildConstraintsFunc for Func +where + Func: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + Size, + &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + ) -> Size, +{ + fn invoke( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + self(available_space, context) + } +} + +trait PositionChildFunc: Send + UnwindSafe { + fn invoke( + &mut self, + size: Size, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> WrappedLayout; +} + +impl PositionChildFunc for Func +where + Func: Send + + UnwindSafe + + 'static + + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + Size, + Size, + &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + ) -> WrappedLayout, +{ + fn invoke( + &mut self, + size: Size, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> WrappedLayout { + self(size, available_space, context) + } +} + +trait EventFunc: Send + UnwindSafe { + fn invoke(&mut self, context: &mut EventContext<'_, '_>) -> R; +} + +impl EventFunc for Func +where + Func: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>) -> R, +{ + fn invoke(&mut self, context: &mut EventContext<'_, '_>) -> R { + self(context) + } +} + +trait OneParamEventFunc: Send + UnwindSafe { + fn invoke(&mut self, param: P, context: &mut EventContext<'_, '_>) -> R; +} + +impl OneParamEventFunc for Func +where + Func: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(P, &mut EventContext<'context, 'window>) -> R, +{ + fn invoke(&mut self, location: P, context: &mut EventContext<'_, '_>) -> R { + self(location, context) + } +} + +trait ThreeParamEventFunc: Send + UnwindSafe { + fn invoke( + &mut self, + location: P1, + device_id: P2, + button: P3, + context: &mut EventContext<'_, '_>, + ) -> R; +} + +type MouseUpFunc = dyn ThreeParamEventFunc>, DeviceId, MouseButton>; + +impl ThreeParamEventFunc for Func +where + Func: Send + + UnwindSafe + + 'static + + for<'context, 'window> FnMut(P1, P2, P3, &mut EventContext<'context, 'window>) -> R, +{ + fn invoke( + &mut self, + location: P1, + device_id: P2, + button: P3, + context: &mut EventContext<'_, '_>, + ) -> R { + self(location, device_id, button, context) + } +} diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 2cf549c..7d040f4 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -40,7 +40,7 @@ impl Label { if *prepared_generation == check_generation && *prepared_color == color && (*prepared_width == width - || (*prepared_width < width + || ((*prepared_width < width || prepared.size.width <= width) && prepared.line_height == prepared.size.height)) => {} _ => { let measured = self.text.map(|text| { diff --git a/src/widgets/space.rs b/src/widgets/space.rs index 0befe0c..a5931a0 100644 --- a/src/widgets/space.rs +++ b/src/widgets/space.rs @@ -41,7 +41,7 @@ impl Widget for Space { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { self.color.redraw_when_changed(context); let color = self.color.get(); - context.gfx.fill(color); + context.fill(color); } fn layout( diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 097b368..7f5a805 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -5,7 +5,9 @@ use std::ops::{Bound, Deref}; use alot::{LotId, OrderedLots}; use kludgine::figures::units::{Lp, UPx}; -use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; +use kludgine::figures::{ + Fraction, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, +}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::Dimension; @@ -343,7 +345,7 @@ impl Layout { self.premeasured.retain(|&measured| measured != id); match min { Dimension::Px(pixels) => { - self.allocated_space.0 -= pixels.into_unsigned(); + self.allocated_space.0 -= pixels.into_unsigned().ceil(); } Dimension::Lp(lp) => { self.allocated_space.1 -= lp; @@ -403,7 +405,8 @@ impl Layout { ) -> Size { let (space_constraint, other_constraint) = self.orientation.split_size(available); let available_space = space_constraint.max(); - let allocated_space = self.allocated_space.0 + self.allocated_space.1.into_upx(scale); + let allocated_space = + self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil(); let mut remaining = available_space.saturating_sub(allocated_space); // If our `other_constraint` is not known, we will need to give child // widgets an opportunity to lay themselves out in the full area. This @@ -442,8 +445,8 @@ impl Layout { // Measure the weighted children within the remaining space if self.total_weights > 0 { - let space_per_weight = remaining / self.total_weights; - remaining %= self.total_weights; + let space_per_weight = (remaining / self.total_weights).floor(); + remaining -= space_per_weight * self.total_weights; for (fractional_index, &(id, weight)) in self.fractional.iter().enumerate() { let index = self.children.index_of_id(id).expect("child not found"); let mut size = space_per_weight * u32::from(weight); @@ -453,7 +456,7 @@ impl Layout { let from_end = u32::try_from(self.fractional.len() - fractional_index) .expect("too many items"); if remaining >= from_end { - let amount = (remaining + from_end - 1) / from_end; + let amount = (remaining / from_end).ceil().min(remaining); remaining -= amount; size += amount; } diff --git a/src/widgets/switcher.rs b/src/widgets/switcher.rs index d4b9ee9..a1b5371 100644 --- a/src/widgets/switcher.rs +++ b/src/widgets/switcher.rs @@ -49,7 +49,7 @@ impl WrapperWidget for Switcher { } // TODO this should be moved to an invalidated() event once we have it. - fn adjust_child_constraint( + fn adjust_child_constraints( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>,