From 64f46a46e28e38d072c6162dfcdcb7919997023d Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 1 Nov 2023 20:11:05 -0700 Subject: [PATCH] Button animations, hover fixes, ComponentType --- examples/counter.rs | 2 +- src/animation.rs | 31 ++++++++++++++ src/context.rs | 58 ++++++++++++++++---------- src/graphics.rs | 4 +- src/styles.rs | 51 +++++++++++++++++++++-- src/styles/components.rs | 22 ++++++++++ src/tree.rs | 70 +++++++++++++++++++++++--------- src/value.rs | 9 ++++ src/widgets.rs | 4 +- src/widgets/button.rs | 88 ++++++++++++++++++++++++++++++++-------- src/widgets/input.rs | 12 +++--- src/widgets/label.rs | 13 ++++-- src/widgets/scroll.rs | 14 +++++-- src/widgets/tilemap.rs | 2 +- src/window.rs | 3 +- 15 files changed, 300 insertions(+), 83 deletions(-) diff --git a/examples/counter.rs b/examples/counter.rs index 629cf8a..15256fa 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -7,7 +7,7 @@ use gooey::{widgets, Run}; fn main() -> gooey::Result { let counter = Dynamic::new(0i32); let label = counter.map_each(ToString::to_string); - Scroll::new(Stack::rows(widgets![ + Scroll::new(Stack::columns(widgets![ Label::new(label), Button::new("+").on_click(counter.with_clone(|counter| { move |_| { diff --git a/src/animation.rs b/src/animation.rs index 0b1c892..4d560e7 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -9,6 +9,7 @@ use std::time::{Duration, Instant}; use alot::{LotId, Lots}; use kempt::Set; +use kludgine::Color; use crate::value::Dynamic; @@ -337,6 +338,25 @@ where } } +impl IntoAnimate for Duration { + type Animate = Self; + + fn into_animate(self) -> Self::Animate { + self + } +} + +impl Animate for Duration { + fn animate(&mut self, elapsed: Duration) -> ControlFlow { + if let Some(remaining) = self.checked_sub(elapsed) { + *self = remaining; + ControlFlow::Continue(()) + } else { + ControlFlow::Break(elapsed - *self) + } + } +} + /// Performs a linear interpolation between two values. pub trait LinearInterpolate { /// Interpolate linearly between `self` and `target` using `percent`. @@ -409,6 +429,17 @@ fn integer_lerps() { test_lerps(&isize::MIN, &isize::MAX, &0); } +impl LinearInterpolate for Color { + fn lerp(&self, target: &Self, percent: f32) -> Self { + Color::new( + self.red().lerp(&target.red(), percent), + self.green().lerp(&target.green(), percent), + self.blue().lerp(&target.blue(), percent), + self.alpha().lerp(&target.alpha(), percent), + ) + } +} + /// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity. /// /// Because of these restrictions, this type implements `Ord` and `Eq`. diff --git a/src/context.rs b/src/context.rs index 6957c79..6ee7f96 100644 --- a/src/context.rs +++ b/src/context.rs @@ -11,7 +11,7 @@ use kludgine::Kludgine; use crate::graphics::Graphics; use crate::styles::components::HighlightColor; -use crate::styles::{ComponentDefaultvalue, Styles}; +use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles}; use crate::value::Dynamic; use crate::widget::{EventHandling, ManagedWidget, WidgetInstance}; use crate::window::RunningWindow; @@ -126,31 +126,29 @@ impl<'context, 'window> EventContext<'context, 'window> { } pub(crate) fn hover(&mut self, location: Point) { - if let Ok(changes) = self.current_node.tree.hover(Some(self.current_node)) { - for unhovered in changes.unhovered { - let mut context = self.for_other(&unhovered); - unhovered.lock().unhover(&mut context); - } - for hover in changes.hovered { - let mut context = self.for_other(&hover); - hover.lock().hover(location, &mut context); - } + let changes = self.current_node.tree.hover(Some(self.current_node)); + for unhovered in changes.unhovered { + let mut context = self.for_other(&unhovered); + unhovered.lock().unhover(&mut context); + } + for hover in changes.hovered { + let mut context = self.for_other(&hover); + hover.lock().hover(location, &mut context); } } pub(crate) fn clear_hover(&mut self) { - if let Ok(changes) = self.current_node.tree.hover(None) { - assert!(changes.hovered.is_empty()); + let changes = self.current_node.tree.hover(None); + assert!(changes.hovered.is_empty()); - for old_hover in changes.unhovered { - let mut old_hover_context = self.for_other(&old_hover); - old_hover.lock().unhover(&mut old_hover_context); - } + for old_hover in changes.unhovered { + let mut old_hover_context = self.for_other(&old_hover); + old_hover.lock().unhover(&mut old_hover_context); } } pub(crate) fn apply_pending_state(&mut self) { - let active = self.pending_state.active.take(); + let active = self.pending_state.active.clone(); 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) => { @@ -164,12 +162,12 @@ impl<'context, 'window> EventContext<'context, 'window> { }; if new { if let Some(active) = active { - active.lock().activate(self); + active.lock().activate(&mut self.for_other(&active)); } } } - let focus = self.pending_state.focus.take(); + let focus = self.pending_state.focus.clone(); 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) => { @@ -183,7 +181,7 @@ impl<'context, 'window> EventContext<'context, 'window> { }; if new { if let Some(focus) = focus { - focus.lock().focus(self); + focus.lock().focus(&mut self.for_other(&focus)); } } } @@ -296,7 +294,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' /// Renders the default focus ring for this widget. pub fn draw_focus_ring(&mut self) { - self.draw_focus_ring_using(&self.query_style(&[&HighlightColor])); + self.draw_focus_ring_using(&self.query_styles(&[&HighlightColor])); } /// Invokes [`Widget::measure()`](crate::widget::Widget::measure) on this @@ -559,7 +557,23 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// 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 { + pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { + self.current_node + .tree + .query_styles(self.current_node, query) + } + + /// Queries the widget hierarchy for a single style component. + /// + /// This function traverses up the widget hierarchy looking for the + /// component being requested. If a matching component is found, it will be + /// returned. Otherwise, the default value will be returned. + + #[must_use] + pub fn query_style( + &self, + query: &Component, + ) -> Component::ComponentType { self.current_node.tree.query_style(self.current_node, query) } } diff --git a/src/graphics.rs b/src/graphics.rs index 8238442..de47bae 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -208,7 +208,7 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { /// used. /// /// `origin` allows controlling how the text will be drawn relative to the - /// coordinate provided in [`render()`](crate::PreparedGraphic::render). + /// coordinate provided in [`render()`](kludgine::PreparedGraphic::render). pub fn draw_text_buffer( &mut self, buffer: &cosmic_text::Buffer, @@ -244,7 +244,7 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> { /// used. /// /// `origin` allows controlling how the text will be drawn relative to the - /// coordinate provided in [`render()`](crate::PreparedGraphic::render). + /// coordinate provided in [`render()`](kludgine::PreparedGraphic::render). pub fn draw_measured_text( &mut self, text: &MeasuredText, diff --git a/src/styles.rs b/src/styles.rs index 9c11d44..6c0714f 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -71,7 +71,9 @@ impl Styles { self.0 .get(&name.group) .and_then(|group| group.get(&name.name)) - .and_then(|component| component.clone().try_into().ok()) + .and_then(|component| { + ::try_from_component(component.clone()).ok() + }) .unwrap_or_else(|| component.default_value()) } } @@ -143,7 +145,7 @@ pub enum Component { /// A percentage between 0.0 and 1.0. Percent(f32), /// A custom component type. - Boxed(CustomComponent), + Custom(CustomComponent), } impl From for Component { @@ -223,6 +225,12 @@ pub enum Dimension { Lp(Lp), } +impl Default for Dimension { + fn default() -> Self { + Self::Px(Px(0)) + } +} + impl From for Dimension { fn from(value: Px) -> Self { Self::Px(value) @@ -286,6 +294,19 @@ impl CustomComponent { } } +impl ComponentType for CustomComponent { + fn into_component(self) -> Component { + Component::Custom(self) + } + + fn try_from_component(component: Component) -> Result { + match component { + Component::Custom(custom) => Ok(custom), + other => Err(other), + } + } +} + trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug { fn as_any(&self) -> &dyn Any; } @@ -378,12 +399,34 @@ pub trait NamedComponent { /// Rust type. pub trait ComponentDefinition: NamedComponent { /// The type that will be contained in the [`Component`]. - type ComponentType: Into + TryFrom; + type ComponentType: ComponentType; /// Returns the default value to use for this component. fn default_value(&self) -> Self::ComponentType; } +/// A type that can be converted to and from [`Component`]. +pub trait ComponentType: Sized { + /// Returns this type, wrapped in a [`Component`]. + fn into_component(self) -> Component; + /// Attempts to extract this type from `component`. If `component` does not + /// contain this type, `Err(component)` is returned. + fn try_from_component(component: Component) -> Result; +} + +impl ComponentType for T +where + T: Into + TryFrom, +{ + fn into_component(self) -> Component { + self.into() + } + + fn try_from_component(component: Component) -> Result { + Self::try_from(component) + } +} + /// A type that represents a named component with a default value. pub trait ComponentDefaultvalue: NamedComponent { /// Returns the default value for this component. @@ -395,7 +438,7 @@ where T: ComponentDefinition, { fn default_component_value(&self) -> Component { - self.default_value().into() + self.default_value().into_component() } } diff --git a/src/styles/components.rs b/src/styles/components.rs index 63cd5c3..e5f47ad 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -77,3 +77,25 @@ impl ComponentDefinition for HighlightColor { Color::AQUA } } + +/// Intrinsic, uniform padding for a widget. +/// +/// This component is opt-in and does not automatically work for all widgets. To +/// apply arbitrary, non-uniform padding around another widget, use a +/// [`Cell`](crate::widgets::Cell). +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct IntrinsicPadding; + +impl NamedComponent for IntrinsicPadding { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("padding")) + } +} + +impl ComponentDefinition for IntrinsicPadding { + type ComponentType = Dimension; + + fn default_value(&self) -> Dimension { + Dimension::Lp(Lp::points(5)) + } +} diff --git a/src/tree.rs b/src/tree.rs index 660e730..e474728 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -6,7 +6,7 @@ use alot::{LotId, Lots}; use kludgine::figures::units::Px; use kludgine::figures::{Point, Rect}; -use crate::styles::{ComponentDefaultvalue, Styles}; +use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles}; use crate::widget::{ManagedWidget, WidgetInstance}; #[derive(Clone, Default)] @@ -61,31 +61,26 @@ impl Tree { data.render_order.clear(); } - pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> Result { + pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> HoverResults { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - let mut hovered = new_hover + let hovered = new_hover .map(|new_hover| data.widget_hierarchy(new_hover.id, self)) .unwrap_or_default(); - match data.update_tracked_widget(new_hover, self, |data| &mut data.hover)? { - Some(old_hover) => { + let unhovered = match data.update_tracked_widget(new_hover, self, |data| &mut data.hover) { + Ok(Some(old_hover)) => { let mut old_hovered = data.widget_hierarchy(old_hover.id, self); // For any widgets that were shared, remove them, as they don't // need to have their events fired again. - while !old_hovered.is_empty() && old_hovered.get(0) == hovered.get(0) { + let mut new_index = 0; + while !old_hovered.is_empty() && old_hovered.get(0) == hovered.get(new_index) { old_hovered.remove(0); - hovered.remove(0); + new_index += 1; } - - Ok(HoverResults { - unhovered: old_hovered, - hovered, - }) + old_hovered } - None => Ok(HoverResults { - unhovered: Vec::new(), - hovered, - }), - } + _ => Vec::new(), + }; + HoverResults { unhovered, hovered } } pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result, ()> { @@ -167,7 +162,7 @@ impl Tree { data.nodes[id.0].styles = Some(styles); } - pub fn query_style( + pub fn query_styles( &self, perspective: &ManagedWidget, query: &[&dyn ComponentDefaultvalue], @@ -175,7 +170,18 @@ impl Tree { self.data .lock() .map_or_else(PoisonError::into_inner, |g| g) - .query_style(perspective.id, query) + .query_styles(perspective.id, query) + } + + pub fn query_style( + &self, + perspective: &ManagedWidget, + component: &Component, + ) -> Component::ComponentType { + self.data + .lock() + .map_or_else(PoisonError::into_inner, |g| g) + .query_style(perspective.id, component) } } @@ -255,7 +261,7 @@ impl TreeData { } } - fn query_style( + fn query_styles( &self, mut perspective: WidgetId, query: &[&dyn ComponentDefaultvalue], @@ -279,6 +285,30 @@ impl TreeData { } resolved } + + fn query_style( + &self, + mut perspective: WidgetId, + query: &Component, + ) -> Component::ComponentType { + let name = query.name(); + loop { + let node = &self.nodes[perspective.0]; + if let Some(styles) = &node.styles { + if let Some(component) = styles.get(&name) { + let Ok(value) = + ::try_from_component(component.clone()) + else { + break; + }; + return value; + } + } + let Some(parent) = node.parent else { break }; + perspective = parent; + } + query.default_value() + } } pub struct Node { diff --git a/src/value.rs b/src/value.rs index ef19cd8..fdfab23 100644 --- a/src/value.rs +++ b/src/value.rs @@ -433,6 +433,15 @@ impl Value { } } +impl Default for Value +where + T: Default, +{ + fn default() -> Self { + Self::Constant(T::default()) + } +} + /// A type that can be converted into a [`Value`]. pub trait IntoValue { /// Returns this type as a [`Value`]. diff --git a/src/widgets.rs b/src/widgets.rs index 48ff140..0404d1c 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,10 +1,10 @@ //! Built-in [`Widget`](crate::widget::Widget) implementations. -mod button; +pub mod button; mod canvas; mod input; mod label; -mod scroll; +pub mod scroll; pub mod stack; mod style; mod tilemap; diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 6ae8ee2..481f4fe 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,19 +1,22 @@ +//! A clickable, labeled button use std::borrow::Cow; use std::panic::UnwindSafe; +use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; use kludgine::app::winit::keyboard::KeyCode; use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{IntoUnsigned, Point, Rect, Size}; +use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::shapes::Shape; use kludgine::text::Text; use kludgine::Color; -use crate::context::{EventContext, GraphicsContext}; +use crate::animation::{Animation, AnimationHandle, Spawn}; +use crate::context::{EventContext, GraphicsContext, WidgetContext}; use crate::names::Name; -use crate::styles::components::{HighlightColor, TextColor}; +use crate::styles::components::{HighlightColor, IntrinsicPadding, TextColor}; use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; -use crate::value::{IntoValue, Value}; +use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; /// A clickable button. @@ -24,6 +27,8 @@ pub struct Button { /// The callback that is invoked when the button is clicked. pub on_click: Option>, buttons_pressed: usize, + background_color: Option>, + background_color_animation: AnimationHandle, } impl Button { @@ -33,6 +38,8 @@ impl Button { label: label.into_value(), on_click: None, buttons_pressed: 0, + background_color: None, + background_color_animation: AnimationHandle::default(), } } @@ -53,6 +60,50 @@ impl Button { on_click.invoke(()); } } + + fn update_background_color(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { + let styles = context.query_styles(&[ + &ButtonActiveBackground, + &ButtonBackground, + &ButtonHoverBackground, + ]); + let background_color = if context.active() { + styles.get_or_default(&ButtonActiveBackground) + } else if context.hovered() { + styles.get_or_default(&ButtonHoverBackground) + } else { + styles.get_or_default(&ButtonBackground) + }; + + match (immediate, &self.background_color) { + (false, Some(dynamic)) => { + self.background_color_animation = Animation::linear( + dynamic.clone(), + background_color, + Duration::from_millis(150), + ) + .spawn(); + } + (true, Some(dynamic)) => { + dynamic.set(background_color); + self.background_color_animation.clear(); + } + (_, None) => { + let dynamic = Dynamic::new(background_color); + self.background_color = Some(dynamic); + } + } + } + + fn current_background_color(&mut self, context: &WidgetContext<'_, '_>) -> Color { + if self.background_color.is_none() { + self.update_background_color(context, false); + } + + let background_color = self.background_color.as_ref().expect("always initialized"); + context.redraw_when_changed(background_color); + background_color.get() + } } impl Widget for Button { @@ -61,7 +112,7 @@ impl Widget for Button { let center = Point::from(size) / 2; self.label.redraw_when_changed(context); - let styles = context.query_style(&[ + let styles = context.query_styles(&[ &TextColor, &HighlightColor, &ButtonActiveBackground, @@ -71,13 +122,7 @@ impl Widget for Button { let visible_rect = Rect::from(size - (Px(1), Px(1))); - let background = if context.active() { - styles.get_or_default(&ButtonActiveBackground) - } else if context.hovered() { - styles.get_or_default(&ButtonHoverBackground) - } else { - styles.get_or_default(&ButtonBackground) - }; + let background = self.current_background_color(context); let background = Shape::filled_rect(visible_rect, background); context .graphics @@ -173,6 +218,10 @@ impl Widget for Button { available_space: Size, context: &mut GraphicsContext<'_, '_, '_, '_, '_>, ) -> Size { + let padding = context + .query_style(&IntrinsicPadding) + .into_px(context.graphics.scale()) + .into_unsigned(); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); self.label.map(|label| { let measured = context @@ -180,7 +229,8 @@ impl Widget for Button { .measure_text::(Text::from(label).wrap_at(width)); let mut size = measured.size.into_unsigned(); - size.height = size.height.max(measured.line_height.into_unsigned()); + size.width += padding * 2; + size.height = size.height.max(measured.line_height.into_unsigned()) + padding * 2; size }) } @@ -210,11 +260,11 @@ impl Widget for Button { } fn unhover(&mut self, context: &mut EventContext<'_, '_>) { - context.set_needs_redraw(); + self.update_background_color(context, false); } fn hover(&mut self, _location: Point, context: &mut EventContext<'_, '_>) { - context.set_needs_redraw(); + self.update_background_color(context, false); } fn focus(&mut self, context: &mut EventContext<'_, '_>) { @@ -226,11 +276,11 @@ impl Widget for Button { } fn activate(&mut self, context: &mut EventContext<'_, '_>) { - context.set_needs_redraw(); + self.update_background_color(context, true); } fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { - context.set_needs_redraw(); + self.update_background_color(context, false); } } @@ -240,6 +290,7 @@ impl ComponentGroup for Button { } } +/// The background color of the button. #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct ButtonBackground; @@ -257,6 +308,7 @@ impl ComponentDefinition for ButtonBackground { } } +/// The background color of the button when it is active (depressed). #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct ButtonActiveBackground; @@ -274,6 +326,8 @@ impl ComponentDefinition for ButtonActiveBackground { } } +/// The background color of the button when the mouse cursor is hovering over +/// it. #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct ButtonHoverBackground; diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 6a38efd..7dfd29a 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -80,7 +80,7 @@ impl Input { } fn styles(context: &WidgetContext<'_, '_>) -> Styles { - context.query_style(&[&TextColor, &TextSize, &LineHeight]) + context.query_styles(&[&TextColor, &TextSize, &LineHeight]) } } @@ -105,7 +105,7 @@ impl Widget for Input { context: &mut EventContext<'_, '_>, ) -> EventHandling { context.focus(); - let styles = context.query_style(&[&TextColor]); + let styles = context.query_styles(&[&TextColor]); self.editor_mut(context.kludgine, &styles).action( context.kludgine.font_system(), Action::Click { @@ -124,7 +124,7 @@ impl Widget for Input { _button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_, '_>, ) { - let styles = context.query_style(&[&TextColor]); + let styles = context.query_styles(&[&TextColor]); self.editor_mut(context.kludgine, &styles).action( context.kludgine.font_system(), Action::Drag { @@ -141,7 +141,7 @@ impl Widget for Input { self.cursor_state.update(context.elapsed()); let cursor_state = self.cursor_state; let size = context.graphics.size(); - let styles = context.query_style(&[&TextColor, &HighlightColor]); + let styles = context.query_styles(&[&TextColor, &HighlightColor]); let highlight = styles.get_or_default(&HighlightColor); let editor = self.editor_mut(&mut context.graphics, &styles); let cursor = editor.cursor(); @@ -293,7 +293,7 @@ impl Widget for Input { available_space: kludgine::figures::Size, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, ) -> kludgine::figures::Size { - let styles = context.query_style(&[&TextColor]); + let styles = context.query_styles(&[&TextColor]); let editor = self.editor_mut(&mut context.graphics, &styles); let buffer = editor.buffer_mut(); buffer.set_size( @@ -319,7 +319,7 @@ impl Widget for Input { return IGNORED; } - let styles = context.query_style(&[&TextColor]); + let styles = context.query_styles(&[&TextColor]); let editor = self.editor_mut(context.kludgine, &styles); println!( diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 1713757..7b0ffcf 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -1,9 +1,9 @@ use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{Point, Size}; +use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size}; use kludgine::text::{MeasuredText, Text, TextOrigin}; use crate::context::GraphicsContext; -use crate::styles::components::TextColor; +use crate::styles::components::{IntrinsicPadding, TextColor}; use crate::value::{IntoValue, Value}; use crate::widget::Widget; @@ -31,7 +31,7 @@ impl Widget for Label { let size = context.graphics.region().size; let center = Point::from(size) / 2; - let styles = context.query_style(&[&TextColor]); + let styles = context.query_styles(&[&TextColor]); if let Some(measured) = &self.prepared_text { context @@ -56,12 +56,17 @@ impl Widget for Label { available_space: Size, context: &mut GraphicsContext<'_, '_, '_, '_, '_>, ) -> Size { + let padding = context + .query_style(&IntrinsicPadding) + .into_px(context.graphics.scale()) + .into_unsigned(); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); self.text.map(|contents| { let measured = context .graphics .measure_text(Text::from(contents).wrap_at(width)); - let size = measured.size.try_cast().unwrap_or_default(); + let mut size = measured.size.try_cast().unwrap_or_default(); + size += padding * 2; self.prepared_text = Some(measured); size }) diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 2fe6d17..510a771 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -1,3 +1,4 @@ +//! A container that scrolls its contents on a virtual surface. use std::borrow::Cow; use std::time::Duration; @@ -10,7 +11,7 @@ use kludgine::figures::{ use kludgine::shapes::Shape; use kludgine::Color; -use crate::animation::{Animation, AnimationHandle, Spawn, ZeroToOne}; +use crate::animation::{Animation, AnimationHandle, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext}; use crate::styles::{ ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent, @@ -82,6 +83,12 @@ impl Widget for Scroll { ZeroToOne::ONE, Duration::from_millis(300), ) + .chain(Duration::from_secs(1)) + .chain(Animation::linear( + self.scrollbar_opacity.clone(), + ZeroToOne::ZERO, + Duration::from_millis(300), + )) .spawn(); } @@ -101,7 +108,7 @@ impl Widget for Scroll { return; }; let visible_bottom_right = visible_rect.into_signed().extent(); - let styles = context.query_style(&[&ScrollBarThickness]); + let styles = context.query_styles(&[&ScrollBarThickness]); let bar_width = styles .get_or_default(&ScrollBarThickness) .into_px(context.graphics.scale()); @@ -219,13 +226,14 @@ fn scrollbar_region(scroll: Px, content_size: Px, control_size: Px) -> Scrollbar } } +/// The thickness that scrollbars are drawn with. pub struct ScrollBarThickness; impl ComponentDefinition for ScrollBarThickness { type ComponentType = Dimension; fn default_value(&self) -> Self::ComponentType { - Dimension::Lp(Lp::points(9)) + Dimension::Lp(Lp::points(7)) } } diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index bee86e0..d6524a8 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -27,7 +27,7 @@ impl TileMap { fn construct(layers: Value) -> Self { Self { layers, - focus: Value::Constant(TileMapFocus::default()), + focus: Value::default(), zoom: 1., tick: None, } diff --git a/src/window.rs b/src/window.rs index 5deca9a..b93a5f3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -366,7 +366,8 @@ where ) { match state { ElementState::Pressed => { - WidgetContext::new(&self.root, &mut window).clear_focus(); + EventContext::new(WidgetContext::new(&self.root, &mut window), kludgine) + .clear_focus(); if let (ElementState::Pressed, Some(location), Some(hovered)) = (state, &self.mouse_state.location, &self.mouse_state.widget)