Button animations, hover fixes, ComponentType

This commit is contained in:
Jonathan Johnson 2023-11-01 20:11:05 -07:00
parent 6b8e5f886b
commit 64f46a46e2
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
15 changed files with 300 additions and 83 deletions

View file

@ -7,7 +7,7 @@ use gooey::{widgets, Run};
fn main() -> gooey::Result { fn main() -> gooey::Result {
let counter = Dynamic::new(0i32); let counter = Dynamic::new(0i32);
let label = counter.map_each(ToString::to_string); let label = counter.map_each(ToString::to_string);
Scroll::new(Stack::rows(widgets![ Scroll::new(Stack::columns(widgets![
Label::new(label), Label::new(label),
Button::new("+").on_click(counter.with_clone(|counter| { Button::new("+").on_click(counter.with_clone(|counter| {
move |_| { move |_| {

View file

@ -9,6 +9,7 @@ use std::time::{Duration, Instant};
use alot::{LotId, Lots}; use alot::{LotId, Lots};
use kempt::Set; use kempt::Set;
use kludgine::Color;
use crate::value::Dynamic; 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<Duration> {
if let Some(remaining) = self.checked_sub(elapsed) {
*self = remaining;
ControlFlow::Continue(())
} else {
ControlFlow::Break(elapsed - *self)
}
}
}
/// Performs a linear interpolation between two values. /// Performs a linear interpolation between two values.
pub trait LinearInterpolate { pub trait LinearInterpolate {
/// Interpolate linearly between `self` and `target` using `percent`. /// Interpolate linearly between `self` and `target` using `percent`.
@ -409,6 +429,17 @@ fn integer_lerps() {
test_lerps(&isize::MIN, &isize::MAX, &0); 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. /// 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`. /// Because of these restrictions, this type implements `Ord` and `Eq`.

View file

@ -11,7 +11,7 @@ use kludgine::Kludgine;
use crate::graphics::Graphics; use crate::graphics::Graphics;
use crate::styles::components::HighlightColor; use crate::styles::components::HighlightColor;
use crate::styles::{ComponentDefaultvalue, Styles}; use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles};
use crate::value::Dynamic; use crate::value::Dynamic;
use crate::widget::{EventHandling, ManagedWidget, WidgetInstance}; use crate::widget::{EventHandling, ManagedWidget, WidgetInstance};
use crate::window::RunningWindow; use crate::window::RunningWindow;
@ -126,31 +126,29 @@ impl<'context, 'window> EventContext<'context, 'window> {
} }
pub(crate) fn hover(&mut self, location: Point<Px>) { pub(crate) fn hover(&mut self, location: Point<Px>) {
if let Ok(changes) = self.current_node.tree.hover(Some(self.current_node)) { let changes = self.current_node.tree.hover(Some(self.current_node));
for unhovered in changes.unhovered { for unhovered in changes.unhovered {
let mut context = self.for_other(&unhovered); let mut context = self.for_other(&unhovered);
unhovered.lock().unhover(&mut context); unhovered.lock().unhover(&mut context);
} }
for hover in changes.hovered { for hover in changes.hovered {
let mut context = self.for_other(&hover); let mut context = self.for_other(&hover);
hover.lock().hover(location, &mut context); hover.lock().hover(location, &mut context);
}
} }
} }
pub(crate) fn clear_hover(&mut self) { pub(crate) fn clear_hover(&mut self) {
if let Ok(changes) = self.current_node.tree.hover(None) { let changes = self.current_node.tree.hover(None);
assert!(changes.hovered.is_empty()); assert!(changes.hovered.is_empty());
for old_hover in changes.unhovered { for old_hover in changes.unhovered {
let mut old_hover_context = self.for_other(&old_hover); let mut old_hover_context = self.for_other(&old_hover);
old_hover.lock().unhover(&mut old_hover_context); old_hover.lock().unhover(&mut old_hover_context);
}
} }
} }
pub(crate) fn apply_pending_state(&mut self) { 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) { 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()) { let new = match self.current_node.tree.activate(active.as_ref()) {
Ok(old) => { Ok(old) => {
@ -164,12 +162,12 @@ impl<'context, 'window> EventContext<'context, 'window> {
}; };
if new { if new {
if let Some(active) = active { 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) { 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()) { let new = match self.current_node.tree.focus(focus.as_ref()) {
Ok(old) => { Ok(old) => {
@ -183,7 +181,7 @@ impl<'context, 'window> EventContext<'context, 'window> {
}; };
if new { if new {
if let Some(focus) = focus { 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. /// Renders the default focus ring for this widget.
pub fn draw_focus_ring(&mut self) { 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 /// 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 /// widget is provided as a convenient way to attach styles into the widget
/// hierarchy. /// hierarchy.
#[must_use] #[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<Component: ComponentDefinition>(
&self,
query: &Component,
) -> Component::ComponentType {
self.current_node.tree.query_style(self.current_node, query) self.current_node.tree.query_style(self.current_node, query)
} }
} }

View file

@ -208,7 +208,7 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
/// used. /// used.
/// ///
/// `origin` allows controlling how the text will be drawn relative to the /// `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<Unit>( pub fn draw_text_buffer<Unit>(
&mut self, &mut self,
buffer: &cosmic_text::Buffer, buffer: &cosmic_text::Buffer,
@ -244,7 +244,7 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
/// used. /// used.
/// ///
/// `origin` allows controlling how the text will be drawn relative to the /// `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<Unit>( pub fn draw_measured_text<Unit>(
&mut self, &mut self,
text: &MeasuredText<Unit>, text: &MeasuredText<Unit>,

View file

@ -71,7 +71,9 @@ impl Styles {
self.0 self.0
.get(&name.group) .get(&name.group)
.and_then(|group| group.get(&name.name)) .and_then(|group| group.get(&name.name))
.and_then(|component| component.clone().try_into().ok()) .and_then(|component| {
<Named::ComponentType>::try_from_component(component.clone()).ok()
})
.unwrap_or_else(|| component.default_value()) .unwrap_or_else(|| component.default_value())
} }
} }
@ -143,7 +145,7 @@ pub enum Component {
/// A percentage between 0.0 and 1.0. /// A percentage between 0.0 and 1.0.
Percent(f32), Percent(f32),
/// A custom component type. /// A custom component type.
Boxed(CustomComponent), Custom(CustomComponent),
} }
impl From<Color> for Component { impl From<Color> for Component {
@ -223,6 +225,12 @@ pub enum Dimension {
Lp(Lp), Lp(Lp),
} }
impl Default for Dimension {
fn default() -> Self {
Self::Px(Px(0))
}
}
impl From<Px> for Dimension { impl From<Px> for Dimension {
fn from(value: Px) -> Self { fn from(value: Px) -> Self {
Self::Px(value) 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<Self, Component> {
match component {
Component::Custom(custom) => Ok(custom),
other => Err(other),
}
}
}
trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug { trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
} }
@ -378,12 +399,34 @@ pub trait NamedComponent {
/// Rust type. /// Rust type.
pub trait ComponentDefinition: NamedComponent { pub trait ComponentDefinition: NamedComponent {
/// The type that will be contained in the [`Component`]. /// The type that will be contained in the [`Component`].
type ComponentType: Into<Component> + TryFrom<Component, Error = Component>; type ComponentType: ComponentType;
/// Returns the default value to use for this component. /// Returns the default value to use for this component.
fn default_value(&self) -> Self::ComponentType; 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<Self, Component>;
}
impl<T> ComponentType for T
where
T: Into<Component> + TryFrom<Component, Error = Component>,
{
fn into_component(self) -> Component {
self.into()
}
fn try_from_component(component: Component) -> Result<Self, Component> {
Self::try_from(component)
}
}
/// A type that represents a named component with a default value. /// A type that represents a named component with a default value.
pub trait ComponentDefaultvalue: NamedComponent { pub trait ComponentDefaultvalue: NamedComponent {
/// Returns the default value for this component. /// Returns the default value for this component.
@ -395,7 +438,7 @@ where
T: ComponentDefinition, T: ComponentDefinition,
{ {
fn default_component_value(&self) -> Component { fn default_component_value(&self) -> Component {
self.default_value().into() self.default_value().into_component()
} }
} }

View file

@ -77,3 +77,25 @@ impl ComponentDefinition for HighlightColor {
Color::AQUA 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::<Global>("padding"))
}
}
impl ComponentDefinition for IntrinsicPadding {
type ComponentType = Dimension;
fn default_value(&self) -> Dimension {
Dimension::Lp(Lp::points(5))
}
}

View file

@ -6,7 +6,7 @@ use alot::{LotId, Lots};
use kludgine::figures::units::Px; use kludgine::figures::units::Px;
use kludgine::figures::{Point, Rect}; use kludgine::figures::{Point, Rect};
use crate::styles::{ComponentDefaultvalue, Styles}; use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles};
use crate::widget::{ManagedWidget, WidgetInstance}; use crate::widget::{ManagedWidget, WidgetInstance};
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -61,31 +61,26 @@ impl Tree {
data.render_order.clear(); data.render_order.clear();
} }
pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> Result<HoverResults, ()> { 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 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)) .map(|new_hover| data.widget_hierarchy(new_hover.id, self))
.unwrap_or_default(); .unwrap_or_default();
match data.update_tracked_widget(new_hover, self, |data| &mut data.hover)? { let unhovered = match data.update_tracked_widget(new_hover, self, |data| &mut data.hover) {
Some(old_hover) => { Ok(Some(old_hover)) => {
let mut old_hovered = data.widget_hierarchy(old_hover.id, self); let mut old_hovered = data.widget_hierarchy(old_hover.id, self);
// For any widgets that were shared, remove them, as they don't // For any widgets that were shared, remove them, as they don't
// need to have their events fired again. // 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); old_hovered.remove(0);
hovered.remove(0); new_index += 1;
} }
old_hovered
Ok(HoverResults {
unhovered: old_hovered,
hovered,
})
} }
None => Ok(HoverResults { _ => Vec::new(),
unhovered: Vec::new(), };
hovered, HoverResults { unhovered, hovered }
}),
}
} }
pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> { pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> {
@ -167,7 +162,7 @@ impl Tree {
data.nodes[id.0].styles = Some(styles); data.nodes[id.0].styles = Some(styles);
} }
pub fn query_style( pub fn query_styles(
&self, &self,
perspective: &ManagedWidget, perspective: &ManagedWidget,
query: &[&dyn ComponentDefaultvalue], query: &[&dyn ComponentDefaultvalue],
@ -175,7 +170,18 @@ impl Tree {
self.data self.data
.lock() .lock()
.map_or_else(PoisonError::into_inner, |g| g) .map_or_else(PoisonError::into_inner, |g| g)
.query_style(perspective.id, query) .query_styles(perspective.id, query)
}
pub fn query_style<Component: ComponentDefinition>(
&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, &self,
mut perspective: WidgetId, mut perspective: WidgetId,
query: &[&dyn ComponentDefaultvalue], query: &[&dyn ComponentDefaultvalue],
@ -279,6 +285,30 @@ impl TreeData {
} }
resolved resolved
} }
fn query_style<Component: ComponentDefinition>(
&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) =
<Component::ComponentType>::try_from_component(component.clone())
else {
break;
};
return value;
}
}
let Some(parent) = node.parent else { break };
perspective = parent;
}
query.default_value()
}
} }
pub struct Node { pub struct Node {

View file

@ -433,6 +433,15 @@ impl<T> Value<T> {
} }
} }
impl<T> Default for Value<T>
where
T: Default,
{
fn default() -> Self {
Self::Constant(T::default())
}
}
/// A type that can be converted into a [`Value`]. /// A type that can be converted into a [`Value`].
pub trait IntoValue<T> { pub trait IntoValue<T> {
/// Returns this type as a [`Value`]. /// Returns this type as a [`Value`].

View file

@ -1,10 +1,10 @@
//! Built-in [`Widget`](crate::widget::Widget) implementations. //! Built-in [`Widget`](crate::widget::Widget) implementations.
mod button; pub mod button;
mod canvas; mod canvas;
mod input; mod input;
mod label; mod label;
mod scroll; pub mod scroll;
pub mod stack; pub mod stack;
mod style; mod style;
mod tilemap; mod tilemap;

View file

@ -1,19 +1,22 @@
//! A clickable, labeled button
use std::borrow::Cow; use std::borrow::Cow;
use std::panic::UnwindSafe; use std::panic::UnwindSafe;
use std::time::Duration;
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
use kludgine::app::winit::keyboard::KeyCode; use kludgine::app::winit::keyboard::KeyCode;
use kludgine::figures::units::{Px, UPx}; 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::shapes::Shape;
use kludgine::text::Text; use kludgine::text::Text;
use kludgine::Color; 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::names::Name;
use crate::styles::components::{HighlightColor, TextColor}; use crate::styles::components::{HighlightColor, IntrinsicPadding, TextColor};
use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; 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}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
/// A clickable button. /// A clickable button.
@ -24,6 +27,8 @@ pub struct Button {
/// The callback that is invoked when the button is clicked. /// The callback that is invoked when the button is clicked.
pub on_click: Option<Callback<()>>, pub on_click: Option<Callback<()>>,
buttons_pressed: usize, buttons_pressed: usize,
background_color: Option<Dynamic<Color>>,
background_color_animation: AnimationHandle,
} }
impl Button { impl Button {
@ -33,6 +38,8 @@ impl Button {
label: label.into_value(), label: label.into_value(),
on_click: None, on_click: None,
buttons_pressed: 0, buttons_pressed: 0,
background_color: None,
background_color_animation: AnimationHandle::default(),
} }
} }
@ -53,6 +60,50 @@ impl Button {
on_click.invoke(()); 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 { impl Widget for Button {
@ -61,7 +112,7 @@ impl Widget for Button {
let center = Point::from(size) / 2; let center = Point::from(size) / 2;
self.label.redraw_when_changed(context); self.label.redraw_when_changed(context);
let styles = context.query_style(&[ let styles = context.query_styles(&[
&TextColor, &TextColor,
&HighlightColor, &HighlightColor,
&ButtonActiveBackground, &ButtonActiveBackground,
@ -71,13 +122,7 @@ impl Widget for Button {
let visible_rect = Rect::from(size - (Px(1), Px(1))); let visible_rect = Rect::from(size - (Px(1), Px(1)));
let background = if context.active() { let background = self.current_background_color(context);
styles.get_or_default(&ButtonActiveBackground)
} else if context.hovered() {
styles.get_or_default(&ButtonHoverBackground)
} else {
styles.get_or_default(&ButtonBackground)
};
let background = Shape::filled_rect(visible_rect, background); let background = Shape::filled_rect(visible_rect, background);
context context
.graphics .graphics
@ -173,6 +218,10 @@ impl Widget for Button {
available_space: Size<crate::ConstraintLimit>, available_space: Size<crate::ConstraintLimit>,
context: &mut GraphicsContext<'_, '_, '_, '_, '_>, context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> { ) -> Size<UPx> {
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); let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
self.label.map(|label| { self.label.map(|label| {
let measured = context let measured = context
@ -180,7 +229,8 @@ impl Widget for Button {
.measure_text::<Px>(Text::from(label).wrap_at(width)); .measure_text::<Px>(Text::from(label).wrap_at(width));
let mut size = measured.size.into_unsigned(); 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 size
}) })
} }
@ -210,11 +260,11 @@ impl Widget for Button {
} }
fn unhover(&mut self, context: &mut EventContext<'_, '_>) { fn unhover(&mut self, context: &mut EventContext<'_, '_>) {
context.set_needs_redraw(); self.update_background_color(context, false);
} }
fn hover(&mut self, _location: Point<Px>, context: &mut EventContext<'_, '_>) { fn hover(&mut self, _location: Point<Px>, context: &mut EventContext<'_, '_>) {
context.set_needs_redraw(); self.update_background_color(context, false);
} }
fn focus(&mut self, context: &mut EventContext<'_, '_>) { fn focus(&mut self, context: &mut EventContext<'_, '_>) {
@ -226,11 +276,11 @@ impl Widget for Button {
} }
fn activate(&mut self, context: &mut EventContext<'_, '_>) { fn activate(&mut self, context: &mut EventContext<'_, '_>) {
context.set_needs_redraw(); self.update_background_color(context, true);
} }
fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { 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)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct ButtonBackground; 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)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct ButtonActiveBackground; 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)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct ButtonHoverBackground; pub struct ButtonHoverBackground;

View file

@ -80,7 +80,7 @@ impl Input {
} }
fn styles(context: &WidgetContext<'_, '_>) -> Styles { 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<'_, '_>, context: &mut EventContext<'_, '_>,
) -> EventHandling { ) -> EventHandling {
context.focus(); context.focus();
let styles = context.query_style(&[&TextColor]); let styles = context.query_styles(&[&TextColor]);
self.editor_mut(context.kludgine, &styles).action( self.editor_mut(context.kludgine, &styles).action(
context.kludgine.font_system(), context.kludgine.font_system(),
Action::Click { Action::Click {
@ -124,7 +124,7 @@ impl Widget for Input {
_button: kludgine::app::winit::event::MouseButton, _button: kludgine::app::winit::event::MouseButton,
context: &mut EventContext<'_, '_>, context: &mut EventContext<'_, '_>,
) { ) {
let styles = context.query_style(&[&TextColor]); let styles = context.query_styles(&[&TextColor]);
self.editor_mut(context.kludgine, &styles).action( self.editor_mut(context.kludgine, &styles).action(
context.kludgine.font_system(), context.kludgine.font_system(),
Action::Drag { Action::Drag {
@ -141,7 +141,7 @@ impl Widget for Input {
self.cursor_state.update(context.elapsed()); self.cursor_state.update(context.elapsed());
let cursor_state = self.cursor_state; let cursor_state = self.cursor_state;
let size = context.graphics.size(); 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 highlight = styles.get_or_default(&HighlightColor);
let editor = self.editor_mut(&mut context.graphics, &styles); let editor = self.editor_mut(&mut context.graphics, &styles);
let cursor = editor.cursor(); let cursor = editor.cursor();
@ -293,7 +293,7 @@ impl Widget for Input {
available_space: kludgine::figures::Size<crate::ConstraintLimit>, available_space: kludgine::figures::Size<crate::ConstraintLimit>,
context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>,
) -> kludgine::figures::Size<kludgine::figures::units::UPx> { ) -> kludgine::figures::Size<kludgine::figures::units::UPx> {
let styles = context.query_style(&[&TextColor]); let styles = context.query_styles(&[&TextColor]);
let editor = self.editor_mut(&mut context.graphics, &styles); let editor = self.editor_mut(&mut context.graphics, &styles);
let buffer = editor.buffer_mut(); let buffer = editor.buffer_mut();
buffer.set_size( buffer.set_size(
@ -319,7 +319,7 @@ impl Widget for Input {
return IGNORED; return IGNORED;
} }
let styles = context.query_style(&[&TextColor]); let styles = context.query_styles(&[&TextColor]);
let editor = self.editor_mut(context.kludgine, &styles); let editor = self.editor_mut(context.kludgine, &styles);
println!( println!(

View file

@ -1,9 +1,9 @@
use kludgine::figures::units::{Px, UPx}; 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 kludgine::text::{MeasuredText, Text, TextOrigin};
use crate::context::GraphicsContext; use crate::context::GraphicsContext;
use crate::styles::components::TextColor; use crate::styles::components::{IntrinsicPadding, TextColor};
use crate::value::{IntoValue, Value}; use crate::value::{IntoValue, Value};
use crate::widget::Widget; use crate::widget::Widget;
@ -31,7 +31,7 @@ impl Widget for Label {
let size = context.graphics.region().size; let size = context.graphics.region().size;
let center = Point::from(size) / 2; 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 { if let Some(measured) = &self.prepared_text {
context context
@ -56,12 +56,17 @@ impl Widget for Label {
available_space: Size<crate::ConstraintLimit>, available_space: Size<crate::ConstraintLimit>,
context: &mut GraphicsContext<'_, '_, '_, '_, '_>, context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> { ) -> Size<UPx> {
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); let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
self.text.map(|contents| { self.text.map(|contents| {
let measured = context let measured = context
.graphics .graphics
.measure_text(Text::from(contents).wrap_at(width)); .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); self.prepared_text = Some(measured);
size size
}) })

View file

@ -1,3 +1,4 @@
//! A container that scrolls its contents on a virtual surface.
use std::borrow::Cow; use std::borrow::Cow;
use std::time::Duration; use std::time::Duration;
@ -10,7 +11,7 @@ use kludgine::figures::{
use kludgine::shapes::Shape; use kludgine::shapes::Shape;
use kludgine::Color; 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::context::{AsEventContext, EventContext};
use crate::styles::{ use crate::styles::{
ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent, ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent,
@ -82,6 +83,12 @@ impl Widget for Scroll {
ZeroToOne::ONE, ZeroToOne::ONE,
Duration::from_millis(300), Duration::from_millis(300),
) )
.chain(Duration::from_secs(1))
.chain(Animation::linear(
self.scrollbar_opacity.clone(),
ZeroToOne::ZERO,
Duration::from_millis(300),
))
.spawn(); .spawn();
} }
@ -101,7 +108,7 @@ impl Widget for Scroll {
return; return;
}; };
let visible_bottom_right = visible_rect.into_signed().extent(); 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 let bar_width = styles
.get_or_default(&ScrollBarThickness) .get_or_default(&ScrollBarThickness)
.into_px(context.graphics.scale()); .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; pub struct ScrollBarThickness;
impl ComponentDefinition for ScrollBarThickness { impl ComponentDefinition for ScrollBarThickness {
type ComponentType = Dimension; type ComponentType = Dimension;
fn default_value(&self) -> Self::ComponentType { fn default_value(&self) -> Self::ComponentType {
Dimension::Lp(Lp::points(9)) Dimension::Lp(Lp::points(7))
} }
} }

View file

@ -27,7 +27,7 @@ impl<Layers> TileMap<Layers> {
fn construct(layers: Value<Layers>) -> Self { fn construct(layers: Value<Layers>) -> Self {
Self { Self {
layers, layers,
focus: Value::Constant(TileMapFocus::default()), focus: Value::default(),
zoom: 1., zoom: 1.,
tick: None, tick: None,
} }

View file

@ -366,7 +366,8 @@ where
) { ) {
match state { match state {
ElementState::Pressed => { 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)) = if let (ElementState::Pressed, Some(location), Some(hovered)) =
(state, &self.mouse_state.location, &self.mouse_state.widget) (state, &self.mouse_state.location, &self.mouse_state.widget)