diff --git a/Cargo.lock b/Cargo.lock index a62e7c3..116d98f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,6 +561,7 @@ name = "gooey" version = "0.1.0" dependencies = [ "alot", + "interner", "kludgine", ] @@ -703,6 +704,12 @@ dependencies = [ "hashbrown 0.14.1", ] +[[package]] +name = "interner" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c60687056b35a996f2213287048a7092d801b61df5fee3bd5bd9bf6f17a2d0" + [[package]] name = "io-lifetimes" version = "1.0.11" diff --git a/Cargo.toml b/Cargo.toml index 2af0881..e792858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [ "app", ] } alot = "0.3" +interner = "0.2.1" # appit = { git = "https://github.com/khonsulabs/appit" } [patch."https://github.com/khonsulabs/kludgine"] diff --git a/examples/input.rs b/examples/input.rs new file mode 100644 index 0000000..4393ddd --- /dev/null +++ b/examples/input.rs @@ -0,0 +1,8 @@ +use gooey::dynamic::Dynamic; +use gooey::widget::Widget; +use gooey::widgets::{Button, Input}; +use gooey::EventLoopError; + +fn main() -> Result<(), EventLoopError> { + Input::new("Hello").run() +} diff --git a/examples/style.rs b/examples/style.rs new file mode 100644 index 0000000..d225183 --- /dev/null +++ b/examples/style.rs @@ -0,0 +1,20 @@ +use gooey::children::Children; +use gooey::styles::{Styles, TextColor}; +use gooey::widgets::array::Array; +use gooey::widgets::{Button, Style}; +use gooey::window::Window; +use gooey::EventLoopError; +use kludgine::Color; + +fn main() -> Result<(), EventLoopError> { + Window::for_widget(Array::rows( + Children::new() + .with_widget(Button::new("Default")) + .with_widget(Style::new( + Styles::new().with(&TextColor, Color::RED), + Button::new("Styled"), + )), + )) + .styles(Styles::new().with(&TextColor, Color::GREEN)) + .run() +} diff --git a/src/context.rs b/src/context.rs index 68ac880..5f1a0d7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,13 +1,14 @@ use std::ops::{Deref, DerefMut}; -use kludgine::app::winit::event::{DeviceId, KeyEvent, MouseButton}; +use kludgine::app::winit::event::{DeviceId, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, Point, Rect, Size}; +use kludgine::Kludgine; use crate::dynamic::Dynamic; use crate::graphics::Graphics; -use crate::tree::ManagedWidget; -use crate::widget::{BoxedWidget, EventHandling}; +use crate::styles::{ComponentDefaultvalue, Styles}; +use crate::widget::{BoxedWidget, EventHandling, ManagedWidget}; use crate::window::RunningWindow; use crate::ConstraintLimit; @@ -113,23 +114,41 @@ impl<'context, 'window> Context<'context, 'window> { device_id: DeviceId, input: KeyEvent, is_synthetic: bool, + kludgine: &mut Kludgine, ) -> EventHandling { self.current_node .lock() - .keyboard_input(device_id, input, is_synthetic, self) + .keyboard_input(device_id, input, is_synthetic, kludgine, self) + } + + pub fn mouse_wheel( + &mut self, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + ) -> EventHandling { + self.current_node + .lock() + .mouse_wheel(device_id, delta, phase, self) } #[must_use] - pub fn push_child(&self, child: BoxedWidget) -> ManagedWidget { - self.current_node + pub fn push_child(&mut self, child: BoxedWidget) -> ManagedWidget { + let pushed_widget = self + .current_node .tree - .push_boxed(child, Some(self.current_node)) + .push_boxed(child, Some(self.current_node)); + pushed_widget + .lock() + .mounted(&mut self.for_other(&pushed_widget)); + pushed_widget } - pub fn remove_child(&self, child: ManagedWidget) { + pub fn remove_child(&mut self, child: &ManagedWidget) { self.current_node .tree .remove_child(child, self.current_node); + child.lock().unmounted(&mut self.for_other(child)); } #[must_use] @@ -263,6 +282,15 @@ impl<'context, 'window> Context<'context, 'window> { pub const fn widget(&self) -> &ManagedWidget { self.current_node } + + pub fn attach_styles(&self, styles: Styles) { + self.current_node.attach_styles(styles); + } + + #[must_use] + pub fn query_style(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { + self.current_node.tree.query_style(self.current_node, query) + } } impl Drop for Context<'_, '_> { diff --git a/src/lib.rs b/src/lib.rs index db0bd6a..5cce41d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ pub mod children; pub mod context; pub mod dynamic; pub mod graphics; +pub mod names; +pub mod styles; mod tree; mod utils; pub mod widget; diff --git a/src/names.rs b/src/names.rs new file mode 100644 index 0000000..3b7093f --- /dev/null +++ b/src/names.rs @@ -0,0 +1,35 @@ +use std::borrow::Cow; +use std::ops::Deref; + +use interner::global::{GlobalString, StringPool}; + +static NAMES: StringPool = StringPool::new(); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Name(GlobalString); + +impl Name { + pub fn new<'a>(name: impl Into>) -> Self { + Self(NAMES.get(name)) + } +} + +impl Deref for Name { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> From<&'a str> for Name { + fn from(value: &'a str) -> Self { + Self::new(value) + } +} + +impl From for Name { + fn from(value: String) -> Self { + Self::new(value) + } +} diff --git a/src/styles.rs b/src/styles.rs new file mode 100644 index 0000000..fabfbba --- /dev/null +++ b/src/styles.rs @@ -0,0 +1,315 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::names::Name; +use crate::utils::Lazy; + +#[derive(Clone, Debug, Default)] +pub struct Styles(Arc>>); + +impl Styles { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, name: &impl NamedComponent, component: impl Into) { + let name = name.name().into_owned(); + Arc::make_mut(&mut self.0) + .entry(name.group) + .or_default() + .insert(name.name, component.into()); + } + + #[must_use] + pub fn with(mut self, name: &impl NamedComponent, component: impl Into) -> Self { + self.push(name, component); + self + } + + #[must_use] + pub fn get(&self, component: &Named) -> Option<&Component> + where + Named: NamedComponent + ?Sized, + { + let name = component.name(); + self.0 + .get(&name.group) + .and_then(|group| group.get(&name.name)) + } + + #[must_use] + pub fn get_or_default(&self, component: &Named) -> Named::ComponentType + where + Named: ComponentDefinition + ?Sized, + { + let name = component.name(); + self.0 + .get(&name.group) + .and_then(|group| group.get(&name.name)) + .and_then(|component| component.clone().try_into().ok()) + .unwrap_or_else(|| component.default_value()) + } +} + +pub type StyleQuery = Vec; +use std::any::Any; +use std::fmt::Debug; +use std::panic::{RefUnwindSafe, UnwindSafe}; + +use kludgine::figures::units::{Lp, Px}; +use kludgine::Color; + +#[derive(Debug, Clone)] +pub enum Component { + Color(Color), + Dimension(Dimension), + Percent(f32), + Boxed(BoxedComponent), +} + +impl From for Component { + fn from(value: Color) -> Self { + Self::Color(value) + } +} + +impl TryFrom for Color { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::Color(color) => Ok(color), + other => Err(other), + } + } +} + +impl From for Component { + fn from(value: Dimension) -> Self { + Self::Dimension(value) + } +} + +impl TryFrom for Dimension { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::Dimension(color) => Ok(color), + other => Err(other), + } + } +} + +impl From for Component { + fn from(value: Px) -> Self { + Self::from(Dimension::from(value)) + } +} + +impl TryFrom for Px { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::Dimension(Dimension::Px(px)) => Ok(px), + other => Err(other), + } + } +} + +impl From for Component { + fn from(value: Lp) -> Self { + Self::from(Dimension::from(value)) + } +} + +impl TryFrom for Lp { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::Dimension(Dimension::Lp(px)) => Ok(px), + other => Err(other), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Dimension { + Px(Px), + Lp(Lp), +} + +impl From for Dimension { + fn from(value: Px) -> Self { + Self::Px(value) + } +} + +impl From for Dimension { + fn from(value: Lp) -> Self { + Self::Lp(value) + } +} + +#[derive(Debug, Clone)] +pub struct BoxedComponent(Arc); + +impl BoxedComponent { + pub fn new(value: T) -> Self + where + T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, + { + Self(Arc::new(value)) + } + + #[must_use] + pub fn downcast(&self) -> Option<&T> + where + T: Debug + Send + Sync + 'static, + { + self.0.as_ref().as_any().downcast_ref() + } +} + +trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug { + fn as_any(&self) -> &dyn Any; +} + +impl AnyComponent for T +where + T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Group(Name); + +impl Group { + #[must_use] + pub fn new() -> Self + where + T: ComponentGroup, + { + Self(T::name()) + } + + #[must_use] + pub fn matches(&self) -> bool + where + T: ComponentGroup, + { + self.0 == T::name() + } +} + +pub trait ComponentGroup { + fn name() -> Name; +} + +pub enum Global {} + +impl ComponentGroup for Global { + fn name() -> Name { + Name::new("global") + } +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct ComponentName { + pub group: Group, + pub name: Name, +} + +impl ComponentName { + pub fn new(group: Group, name: impl Into) -> Self { + Self { + group, + name: name.into(), + } + } + + pub fn named(name: impl Into) -> Self { + Self::new(Group::new::(), name) + } +} + +impl From<&'static Lazy> for ComponentName { + fn from(value: &'static Lazy) -> Self { + (**value).clone() + } +} +pub trait NamedComponent { + fn name(&self) -> Cow<'_, ComponentName>; +} + +pub trait ComponentDefinition: NamedComponent { + type ComponentType: Into + TryFrom; + + fn default_value(&self) -> Self::ComponentType; +} + +pub trait ComponentDefaultvalue: NamedComponent { + fn default_component_value(&self) -> Component; +} + +impl ComponentDefaultvalue for T +where + T: ComponentDefinition, +{ + fn default_component_value(&self) -> Component { + self.default_value().into() + } +} + +impl NamedComponent for ComponentName { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Borrowed(self) + } +} + +impl NamedComponent for Cow<'_, ComponentName> { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Borrowed(self) + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct TextColor; + +impl NamedComponent for TextColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("text_color")) + } +} + +impl ComponentDefinition for TextColor { + type ComponentType = Color; + + fn default_value(&self) -> Color { + Color::WHITE + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct HighlightColor; + +impl NamedComponent for HighlightColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("highlight_color")) + } +} + +impl ComponentDefinition for HighlightColor { + type ComponentType = Color; + + fn default_value(&self) -> Color { + Color::AQUA + } +} diff --git a/src/tree.rs b/src/tree.rs index ef6d7ca..91f0ce4 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1,12 +1,13 @@ use std::fmt::Debug; use std::mem; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; +use std::sync::{Arc, Mutex, PoisonError}; use alot::{LotId, Lots}; use kludgine::figures::units::Px; use kludgine::figures::{Point, Rect}; -use crate::widget::{BoxedWidget, Widget}; +use crate::styles::{ComponentDefaultvalue, Styles}; +use crate::widget::{BoxedWidget, ManagedWidget}; #[derive(Clone, Default)] pub struct Tree { @@ -14,13 +15,6 @@ pub struct Tree { } impl Tree { - pub fn push(&self, widget: W, parent: Option<&ManagedWidget>) -> ManagedWidget - where - W: Widget, - { - self.push_boxed(BoxedWidget::new(widget), parent) - } - pub fn push_boxed(&self, widget: BoxedWidget, parent: Option<&ManagedWidget>) -> ManagedWidget { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); let id = WidgetId(data.nodes.push(Node { @@ -28,6 +22,7 @@ impl Tree { children: Vec::new(), parent: parent.map(|parent| parent.id), last_rendered_location: None, + styles: None, })); if let Some(parent) = parent { let parent = &mut data.nodes[parent.id.0]; @@ -40,19 +35,18 @@ impl Tree { } } - #[allow(clippy::needless_pass_by_value)] // This is sort of a destructor type call - pub fn remove_child(&self, child: ManagedWidget, parent: &ManagedWidget) { + pub fn remove_child(&self, child: &ManagedWidget, parent: &ManagedWidget) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); data.remove_child(child.id, parent.id); } - fn note_rendered_rect(&self, widget: WidgetId, rect: Rect) { + pub(crate) fn note_rendered_rect(&self, widget: WidgetId, rect: Rect) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); data.nodes[widget.0].last_rendered_location = Some(rect); data.render_order.push(widget); } - fn last_rendered_at(&self, widget: WidgetId) -> Option> { + pub(crate) fn last_rendered_at(&self, widget: WidgetId) -> Option> { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); data.nodes[widget.0].last_rendered_location } @@ -131,6 +125,22 @@ impl Tree { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); data.nodes[id.0].parent } + + pub(crate) fn attach_styles(&self, id: WidgetId, styles: Styles) { + let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + data.nodes[id.0].styles = Some(styles); + } + + pub fn query_style( + &self, + perspective: &ManagedWidget, + query: &[&dyn ComponentDefaultvalue], + ) -> Styles { + self.data + .lock() + .map_or_else(PoisonError::into_inner, |g| g) + .query_style(perspective.id, query) + } } #[derive(Default)] @@ -180,63 +190,30 @@ impl TreeData { (None, _) => Ok(None), } } -} -#[derive(Clone)] -pub struct ManagedWidget { - pub(crate) id: WidgetId, - pub(crate) widget: BoxedWidget, - pub(crate) tree: Tree, -} - -impl Debug for ManagedWidget { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ManagedWidget") - .field("id", &self.id) - .field("widget", &self.widget) - .finish_non_exhaustive() - } -} - -impl ManagedWidget { - pub(crate) fn lock(&self) -> MutexGuard<'_, dyn Widget> { - self.widget.lock() - } - - pub(crate) fn note_rendered_rect(&self, rect: Rect) { - self.tree.note_rendered_rect(self.id, rect); - } - - pub fn last_rendered_at(&self) -> Option> { - self.tree.last_rendered_at(self.id) - } - - pub fn active(&self) -> bool { - self.tree.active_widget() == Some(self.id) - } - - pub fn hovered(&self) -> bool { - self.tree.hovered_widget() == Some(self.id) - } - - pub fn focused(&self) -> bool { - self.tree.focused_widget() == Some(self.id) - } - - pub fn parent(&self) -> Option { - self.tree.parent(self.id).map(|id| self.tree.widget(id)) - } -} - -impl PartialEq for ManagedWidget { - fn eq(&self, other: &Self) -> bool { - self.widget == other.widget - } -} - -impl PartialEq for ManagedWidget { - fn eq(&self, other: &BoxedWidget) -> bool { - &self.widget == other + fn query_style( + &self, + mut perspective: WidgetId, + query: &[&dyn ComponentDefaultvalue], + ) -> Styles { + let mut query = query.iter().map(|n| n.name()).collect::>(); + let mut resolved = Styles::new(); + while !query.is_empty() { + let node = &self.nodes[perspective.0]; + if let Some(styles) = &node.styles { + query.retain(|name| { + if let Some(component) = styles.get(name) { + resolved.push(name, component.clone()); + false + } else { + true + } + }); + } + let Some(parent) = node.parent else { break }; + perspective = parent; + } + resolved } } @@ -245,6 +222,7 @@ pub struct Node { pub children: Vec, pub parent: Option, pub last_rendered_location: Option>, + pub styles: Option, } #[derive(Clone, Copy, Eq, PartialEq, Debug)] diff --git a/src/utils.rs b/src/utils.rs index ae1c89a..b66d115 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,6 @@ +use std::ops::Deref; +use std::sync::OnceLock; + use kludgine::app::winit::keyboard::ModifiersState; pub trait ModifiersExt { @@ -15,3 +18,25 @@ impl ModifiersExt for ModifiersState { self.control_key() } } + +pub struct Lazy { + init: fn() -> T, + once: OnceLock, +} + +impl Lazy { + pub const fn new(init: fn() -> T) -> Self { + Self { + init, + once: OnceLock::new(), + } + } +} + +impl Deref for Lazy { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.once.get_or_init(self.init) + } +} diff --git a/src/widget.rs b/src/widget.rs index b1cf0ef..6409f18 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -5,13 +5,16 @@ use std::panic::UnwindSafe; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use kludgine::app::winit::error::EventLoopError; -use kludgine::app::winit::event::{DeviceId, KeyEvent, MouseButton}; +use kludgine::app::winit::event::{DeviceId, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{Point, Size}; +use kludgine::figures::{Point, Rect, Size}; +use kludgine::Kludgine; use crate::context::Context; use crate::dynamic::Dynamic; use crate::graphics::Graphics; +use crate::styles::{Component, Group, Styles}; +use crate::tree::{Tree, WidgetId}; use crate::window::{RunningWindow, Window, WindowBehavior}; use crate::ConstraintLimit; @@ -20,7 +23,7 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { where Self: Sized, { - Window::>::new(WidgetWindow(Some(self))).run() + Window::::new(BoxedWidget::new(self)).run() } fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context<'_, '_>); @@ -32,6 +35,11 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { context: &mut Context<'_, '_>, ) -> Size; + #[allow(unused_variables)] + fn mounted(&mut self, context: &mut Context<'_, '_>) {} + #[allow(unused_variables)] + fn unmounted(&mut self, context: &mut Context<'_, '_>) {} + #[allow(unused_variables)] fn hit_test(&mut self, location: Point, context: &mut Context<'_, '_>) -> bool { false @@ -92,10 +100,27 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { device_id: DeviceId, input: KeyEvent, is_synthetic: bool, + kludgine: &mut Kludgine, context: &mut Context<'_, '_>, ) -> EventHandling { UNHANDLED } + + #[allow(unused_variables)] + fn mouse_wheel( + &mut self, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + context: &mut Context<'_, '_>, + ) -> EventHandling { + UNHANDLED + } + + #[allow(unused_variables)] + fn query_component(&self, group: Group, name: &str) -> Option { + None + } } pub type EventHandling = ControlFlow; @@ -108,23 +133,6 @@ pub struct EventIgnored; pub const HANDLED: EventHandling = EventHandling::Break(EventHandled); pub const UNHANDLED: EventHandling = EventHandling::Continue(EventIgnored); -struct WidgetWindow(Option); - -impl WindowBehavior for WidgetWindow -where - T: Widget + Send + UnwindSafe, -{ - type Context = Self; - - fn initialize(_window: &mut RunningWindow<'_>, context: Self::Context) -> Self { - context - } - - fn make_root(&mut self, tree: &crate::tree::Tree) -> crate::tree::ManagedWidget { - tree.push(self.0.take().expect("root already created"), None) - } -} - #[derive(Clone, Debug)] pub struct BoxedWidget(Arc>); @@ -149,6 +157,18 @@ impl PartialEq for BoxedWidget { } } +impl WindowBehavior for BoxedWidget { + type Context = Self; + + fn initialize(_window: &mut RunningWindow<'_>, context: Self::Context) -> Self { + context + } + + fn make_root(&mut self) -> BoxedWidget { + self.clone() + } +} + #[derive(Debug)] pub enum Value where @@ -244,3 +264,70 @@ where self(value); } } + +#[derive(Clone)] +pub struct ManagedWidget { + pub(crate) id: WidgetId, + pub(crate) widget: BoxedWidget, + pub(crate) tree: Tree, +} + +impl Debug for ManagedWidget { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ManagedWidget") + .field("id", &self.id) + .field("widget", &self.widget) + .finish_non_exhaustive() + } +} + +impl ManagedWidget { + pub(crate) fn lock(&self) -> MutexGuard<'_, dyn Widget> { + self.widget.lock() + } + + pub(crate) fn note_rendered_rect(&self, rect: Rect) { + self.tree.note_rendered_rect(self.id, rect); + } + + #[must_use] + pub fn last_rendered_at(&self) -> Option> { + self.tree.last_rendered_at(self.id) + } + + #[must_use] + pub fn active(&self) -> bool { + self.tree.active_widget() == Some(self.id) + } + + #[must_use] + pub fn hovered(&self) -> bool { + self.tree.hovered_widget() == Some(self.id) + } + + #[must_use] + pub fn focused(&self) -> bool { + self.tree.focused_widget() == Some(self.id) + } + + #[must_use] + pub fn parent(&self) -> Option { + self.tree.parent(self.id).map(|id| self.tree.widget(id)) + } + + pub(crate) fn attach_styles(&self, styles: Styles) { + self.tree.attach_styles(self.id, styles); + } +} + +impl PartialEq for ManagedWidget { + fn eq(&self, other: &Self) -> bool { + self.widget == other.widget + } +} + +impl PartialEq for ManagedWidget { + fn eq(&self, other: &BoxedWidget) -> bool { + &self.widget == other + } +} diff --git a/src/widgets.rs b/src/widgets.rs index 73a582a..eaeeb04 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,8 +1,12 @@ pub mod array; mod button; mod canvas; +mod input; mod label; +mod style; pub use button::Button; pub use canvas::Canvas; +pub use input::Input; pub use label::Label; +pub use style::Style; diff --git a/src/widgets/array.rs b/src/widgets/array.rs index 0f77f75..24c3bac 100644 --- a/src/widgets/array.rs +++ b/src/widgets/array.rs @@ -7,8 +7,7 @@ use kludgine::figures::{Point, Rect, Size}; use crate::children::Children; use crate::context::Context; use crate::graphics::Graphics; -use crate::tree::ManagedWidget; -use crate::widget::{IntoValue, Value, Widget}; +use crate::widget::{IntoValue, ManagedWidget, Value, Widget}; use crate::ConstraintLimit; #[derive(Debug)] @@ -83,7 +82,7 @@ impl Array { // Any children remaining at the end of this process are ones // that have been removed. for removed in self.synced_children.drain(children.len()..) { - context.remove_child(removed); + context.remove_child(&removed); } self.layout.truncate(children.len()); }); diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 084f7a2..df45739 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,14 +1,19 @@ +use std::borrow::Cow; use std::panic::UnwindSafe; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; use kludgine::app::winit::keyboard::KeyCode; use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{Point, Rect, Size}; +use kludgine::figures::{IntoUnsigned, Point, Rect, Size}; use kludgine::shapes::{Shape, StrokeOptions}; -use kludgine::Color; +use kludgine::{Color, Kludgine}; use crate::context::Context; use crate::graphics::Graphics; +use crate::names::Name; +use crate::styles::{ + ComponentDefinition, ComponentGroup, ComponentName, HighlightColor, NamedComponent, TextColor, +}; use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED}; #[derive(Debug)] @@ -50,21 +55,32 @@ impl Widget for Button { context.redraw_when_changed(label); } + let styles = context.query_style(&[ + &TextColor, + &HighlightColor, + &ButtonActiveBackground, + &ButtonBackground, + &ButtonHoverBackground, + ]); + let visible_rect = Rect::from(graphics.size() - (UPx(1), UPx(1))); let background = if context.active() { - Color::new(30, 30, 30, 255) + styles.get_or_default(&ButtonActiveBackground) } else if context.hovered() { - Color::new(40, 40, 40, 255) + styles.get_or_default(&ButtonHoverBackground) } else { - Color::new(10, 10, 10, 255) + styles.get_or_default(&ButtonBackground) }; let background = Shape::filled_rect(visible_rect, background); graphics.draw_shape(&background, Point::default(), None, None); if context.focused() { - let focus_ring = - Shape::stroked_rect(visible_rect, Color::AQUA, StrokeOptions::default()); + let focus_ring = Shape::stroked_rect( + visible_rect, + styles.get_or_default(&HighlightColor), + StrokeOptions::default(), + ); graphics.draw_shape(&focus_ring, Point::default(), None, None); } @@ -72,7 +88,7 @@ impl Widget for Button { self.label.map(|label| { graphics.draw_text( label, - Color::WHITE, + styles.get_or_default(&TextColor), kludgine::text::TextOrigin::Center, center, None, @@ -159,11 +175,11 @@ impl Widget for Button { ) -> Size { let width = available_space.width.max().try_into().unwrap_or(Px::MAX); self.label.map(|label| { - graphics - .measure_text::(label, Color::RED, Some(width)) - .size - .try_cast::() - .unwrap_or_default() + let measured = graphics.measure_text::(label, Color::WHITE, Some(width)); + + let mut size = measured.size.into_unsigned(); + size.height = size.height.max(measured.line_height.into_unsigned()); + size }) } @@ -172,6 +188,7 @@ impl Widget for Button { _device_id: DeviceId, input: KeyEvent, _is_synthetic: bool, + kludgine: &mut Kludgine, context: &mut Context<'_, '_>, ) -> EventHandling { if input.physical_key == KeyCode::Space { @@ -215,3 +232,60 @@ impl Widget for Button { context.set_needs_redraw(); } } + +impl ComponentGroup for Button { + fn name() -> Name { + Name::new("button") + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct ButtonBackground; + +impl NamedComponent for ButtonBackground { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::