Progress towards an input widget

This commit is contained in:
Jonathan Johnson 2023-10-18 15:44:13 -07:00
parent e04b1b14ad
commit 87578e5c76
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
18 changed files with 1016 additions and 206 deletions

7
Cargo.lock generated
View file

@ -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"

View file

@ -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"]

8
examples/input.rs Normal file
View file

@ -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()
}

20
examples/style.rs Normal file
View file

@ -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()
}

View file

@ -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<'_, '_> {

View file

@ -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;

35
src/names.rs Normal file
View file

@ -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<Cow<'a, str>>) -> 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<String> for Name {
fn from(value: String) -> Self {
Self::new(value)
}
}

315
src/styles.rs Normal file
View file

@ -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<HashMap<Group, HashMap<Name, Component>>>);
impl Styles {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, name: &impl NamedComponent, component: impl Into<Component>) {
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<Component>) -> Self {
self.push(name, component);
self
}
#[must_use]
pub fn get<Named>(&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<Named>(&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<ComponentName>;
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<Color> for Component {
fn from(value: Color) -> Self {
Self::Color(value)
}
}
impl TryFrom<Component> for Color {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Color(color) => Ok(color),
other => Err(other),
}
}
}
impl From<Dimension> for Component {
fn from(value: Dimension) -> Self {
Self::Dimension(value)
}
}
impl TryFrom<Component> for Dimension {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Dimension(color) => Ok(color),
other => Err(other),
}
}
}
impl From<Px> for Component {
fn from(value: Px) -> Self {
Self::from(Dimension::from(value))
}
}
impl TryFrom<Component> for Px {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Dimension(Dimension::Px(px)) => Ok(px),
other => Err(other),
}
}
}
impl From<Lp> for Component {
fn from(value: Lp) -> Self {
Self::from(Dimension::from(value))
}
}
impl TryFrom<Component> for Lp {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
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<Px> for Dimension {
fn from(value: Px) -> Self {
Self::Px(value)
}
}
impl From<Lp> for Dimension {
fn from(value: Lp) -> Self {
Self::Lp(value)
}
}
#[derive(Debug, Clone)]
pub struct BoxedComponent(Arc<dyn AnyComponent>);
impl BoxedComponent {
pub fn new<T>(value: T) -> Self
where
T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static,
{
Self(Arc::new(value))
}
#[must_use]
pub fn downcast<T>(&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<T> 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<T>() -> Self
where
T: ComponentGroup,
{
Self(T::name())
}
#[must_use]
pub fn matches<T>(&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<Name>) -> Self {
Self {
group,
name: name.into(),
}
}
pub fn named<G: ComponentGroup>(name: impl Into<Name>) -> Self {
Self::new(Group::new::<G>(), name)
}
}
impl From<&'static Lazy<ComponentName>> for ComponentName {
fn from(value: &'static Lazy<ComponentName>) -> Self {
(**value).clone()
}
}
pub trait NamedComponent {
fn name(&self) -> Cow<'_, ComponentName>;
}
pub trait ComponentDefinition: NamedComponent {
type ComponentType: Into<Component> + TryFrom<Component, Error = Component>;
fn default_value(&self) -> Self::ComponentType;
}
pub trait ComponentDefaultvalue: NamedComponent {
fn default_component_value(&self) -> Component;
}
impl<T> 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::<Global>("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::<Global>("highlight_color"))
}
}
impl ComponentDefinition for HighlightColor {
type ComponentType = Color;
fn default_value(&self) -> Color {
Color::AQUA
}
}

View file

@ -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<W>(&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<Px>) {
pub(crate) fn note_rendered_rect(&self, widget: WidgetId, rect: Rect<Px>) {
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<Rect<Px>> {
pub(crate) fn last_rendered_at(&self, widget: WidgetId) -> Option<Rect<Px>> {
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<Px>) {
self.tree.note_rendered_rect(self.id, rect);
}
pub fn last_rendered_at(&self) -> Option<Rect<Px>> {
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<ManagedWidget> {
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<BoxedWidget> 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::<Vec<_>>();
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<WidgetId>,
pub parent: Option<WidgetId>,
pub last_rendered_location: Option<Rect<Px>>,
pub styles: Option<Styles>,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]

View file

@ -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<T> {
init: fn() -> T,
once: OnceLock<T>,
}
impl<T> Lazy<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
init,
once: OnceLock::new(),
}
}
}
impl<T> Deref for Lazy<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.once.get_or_init(self.init)
}
}

View file

@ -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::<WidgetWindow<Self>>::new(WidgetWindow(Some(self))).run()
Window::<BoxedWidget>::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<UPx>;
#[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<Px>, 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<Component> {
None
}
}
pub type EventHandling = ControlFlow<EventHandled, EventIgnored>;
@ -108,23 +133,6 @@ pub struct EventIgnored;
pub const HANDLED: EventHandling = EventHandling::Break(EventHandled);
pub const UNHANDLED: EventHandling = EventHandling::Continue(EventIgnored);
struct WidgetWindow<W>(Option<W>);
impl<T> WindowBehavior for WidgetWindow<T>
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<Mutex<dyn Widget>>);
@ -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<T>
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<Px>) {
self.tree.note_rendered_rect(self.id, rect);
}
#[must_use]
pub fn last_rendered_at(&self) -> Option<Rect<Px>> {
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<ManagedWidget> {
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<BoxedWidget> for ManagedWidget {
fn eq(&self, other: &BoxedWidget) -> bool {
&self.widget == other
}
}

View file

@ -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;

View file

@ -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());
});

View file

@ -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<UPx> {
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
self.label.map(|label| {
graphics
.measure_text::<Px>(label, Color::RED, Some(width))
.size
.try_cast::<UPx>()
.unwrap_or_default()
let measured = graphics.measure_text::<Px>(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::<Button>("background_color"))
}
}
impl ComponentDefinition for ButtonBackground {
type ComponentType = Color;
fn default_value(&self) -> Color {
Color::new(10, 10, 10, 255)
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct ButtonActiveBackground;
impl NamedComponent for ButtonActiveBackground {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Button>("active_background_color"))
}
}
impl ComponentDefinition for ButtonActiveBackground {
type ComponentType = Color;
fn default_value(&self) -> Color {
Color::new(30, 30, 30, 255)
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct ButtonHoverBackground;
impl NamedComponent for ButtonHoverBackground {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Button>("hover_background_color"))
}
}
impl ComponentDefinition for ButtonHoverBackground {
type ComponentType = Color;
fn default_value(&self) -> Color {
Color::new(40, 40, 40, 255)
}
}

169
src/widgets/input.rs Normal file
View file

@ -0,0 +1,169 @@
use std::fmt::Debug;
use kludgine::app::winit::keyboard::Key;
use kludgine::cosmic_text::{Action, Attrs, Buffer, Edit, Editor, FontSystem, Metrics, Shaping};
use kludgine::figures::units::Px;
use kludgine::figures::{FloatConversion, IntoUnsigned, Point};
use kludgine::text::TextOrigin;
use kludgine::{Color, Kludgine};
use crate::styles::{Styles, TextColor};
use crate::utils::ModifiersExt;
use crate::widget::{EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED};
#[must_use]
pub struct Input {
pub text: Value<String>,
editor: Option<LiveEditor>,
}
impl Input {
pub fn empty() -> Self {
Self::new(String::new())
}
pub fn new(initial_text: impl IntoValue<String>) -> Self {
Self {
text: initial_text.into_value(),
editor: None,
}
}
fn editor_mut(&mut self, font_system: &mut FontSystem, styles: &Styles) -> &mut Editor {
match (&self.editor, self.text.generation()) {
(Some(editor), generation) if editor.generation == generation => {}
(_, generation) => {
let mut buffer = Buffer::new(font_system, Metrics::new(12., 18.));
self.text.map(|text| {
buffer.set_text(font_system, text, Attrs::new(), Shaping::Advanced);
});
self.editor = Some(LiveEditor {
editor: Editor::new(buffer),
generation,
});
}
}
&mut self.editor.as_mut().expect("just initialized").editor
}
}
impl Debug for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Input")
.field("text", &self.text)
.finish_non_exhaustive()
}
}
impl Widget for Input {
fn hit_test(
&mut self,
location: Point<Px>,
context: &mut crate::context::Context<'_, '_>,
) -> bool {
true
}
fn mouse_down(
&mut self,
location: Point<Px>,
device_id: kludgine::app::winit::event::DeviceId,
button: kludgine::app::winit::event::MouseButton,
context: &mut crate::context::Context<'_, '_>,
) -> EventHandling {
// self.editor_mut(, styles);
HANDLED
}
fn mouse_up(
&mut self,
location: Option<Point<Px>>,
device_id: kludgine::app::winit::event::DeviceId,
button: kludgine::app::winit::event::MouseButton,
context: &mut crate::context::Context<'_, '_>,
) {
context.focus();
}
fn redraw(
&mut self,
graphics: &mut crate::graphics::Graphics<'_, '_, '_>,
context: &mut crate::context::Context<'_, '_>,
) {
let size = graphics.size();
let styles = context.query_style(&[&TextColor]);
let editor = self.editor_mut(graphics.font_system(), &styles);
let buffer = editor.buffer_mut();
buffer.set_size(
graphics.font_system(),
size.width.into_float(),
size.height.into_float(),
);
buffer.shape_until_scroll(graphics.font_system());
graphics.draw_text_buffer(
buffer,
styles.get_or_default(&TextColor),
TextOrigin::TopLeft,
Point::<Px>::default(),
None,
None,
);
}
fn measure(
&mut self,
available_space: kludgine::figures::Size<crate::ConstraintLimit>,
graphics: &mut crate::graphics::Graphics<'_, '_, '_>,
context: &mut crate::context::Context<'_, '_>,
) -> kludgine::figures::Size<kludgine::figures::units::UPx> {
let styles = context.query_style(&[&TextColor]);
let editor = self.editor_mut(graphics.font_system(), &styles);
let buffer = editor.buffer_mut();
buffer.set_size(
graphics.font_system(),
available_space.width.max().into_float(),
available_space.height.max().into_float(),
);
graphics
.measure_text_buffer::<Px>(buffer, Color::WHITE)
.size
.into_unsigned()
}
fn keyboard_input(
&mut self,
device_id: kludgine::app::winit::event::DeviceId,
input: kludgine::app::winit::event::KeyEvent,
is_synthetic: bool,
kludgine: &mut Kludgine,
context: &mut crate::context::Context<'_, '_>,
) -> EventHandling {
if !input.state.is_pressed() {
return UNHANDLED;
}
let styles = context.query_style(&[&TextColor]);
let editor = &mut self.editor.as_mut().expect("input without editor").editor;
match (input.logical_key, input.text) {
(Key::Backspace, _) => {
editor.action(kludgine.font_system(), Action::Backspace);
context.set_needs_redraw();
HANDLED
}
(_, Some(text)) if !context.modifiers().state().primary() => {
editor.insert_string(&text, None);
context.set_needs_redraw();
HANDLED
}
(_, _) => UNHANDLED,
}
}
}
struct LiveEditor {
editor: Editor,
generation: Option<usize>,
}

View file

@ -5,6 +5,7 @@ use kludgine::Color;
use crate::context::Context;
use crate::graphics::Graphics;
use crate::styles::TextColor;
use crate::widget::{IntoValue, Value, Widget};
#[derive(Debug)]
@ -26,11 +27,12 @@ impl Widget for Label {
if let Value::Dynamic(contents) = &mut self.contents {
context.redraw_when_changed(contents);
}
let styles = context.query_style(&[&TextColor]);
let width = graphics.size().width;
self.contents.map(|contents| {
graphics.draw_text(
contents,
Color::RED,
styles.get_or_default(&TextColor),
TextOrigin::Center,
center,
None,

68
src/widgets/style.rs Normal file
View file

@ -0,0 +1,68 @@
use kludgine::figures::units::UPx;
use kludgine::figures::Size;
use crate::context::Context;
use crate::graphics::Graphics;
use crate::styles::Styles;
use crate::widget::{BoxedWidget, ManagedWidget, Widget};
use crate::ConstraintLimit;
#[derive(Debug)]
pub struct Style {
styles: Styles,
child: BoxedWidget,
mounted_child: Option<ManagedWidget>,
}
impl Style {
pub fn new<W>(styles: Styles, child: W) -> Self
where
W: Widget,
{
Self {
styles,
child: BoxedWidget::new(child),
mounted_child: None,
}
}
}
impl Widget for Style {
fn mounted(&mut self, context: &mut Context<'_, '_>) {
context.attach_styles(self.styles.clone());
self.mounted_child = Some(context.push_child(self.child.clone()));
}
fn unmounted(&mut self, context: &mut Context<'_, '_>) {
let child = self
.mounted_child
.take()
.expect("unmounted without being mounted");
context.remove_child(&child);
}
fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context<'_, '_>) {
context
.for_other(
self.mounted_child
.as_ref()
.expect("measuring without being mounted"),
)
.redraw(graphics);
}
fn measure(
&mut self,
available_space: Size<ConstraintLimit>,
graphics: &mut Graphics<'_, '_, '_>,
context: &mut Context<'_, '_>,
) -> Size<UPx> {
context
.for_other(
self.mounted_child
.as_ref()
.expect("measuring without being mounted"),
)
.measure(available_space, graphics)
}
}

View file

@ -4,29 +4,35 @@ use std::panic::{AssertUnwindSafe, UnwindSafe};
use kludgine::app::winit::dpi::PhysicalPosition;
use kludgine::app::winit::error::EventLoopError;
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
use kludgine::app::winit::event::{
DeviceId, ElementState, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
use kludgine::app::winit::keyboard::KeyCode;
use kludgine::app::WindowBehavior as _;
use kludgine::figures::units::Px;
use kludgine::figures::Point;
use kludgine::render::Drawing;
use kludgine::Kludgine;
use crate::context::Context;
use crate::graphics::Graphics;
use crate::tree::{ManagedWidget, Tree};
use crate::styles::Styles;
use crate::tree::Tree;
use crate::utils::ModifiersExt;
use crate::widget::{EventHandling, HANDLED, UNHANDLED};
use crate::widget::{BoxedWidget, EventHandling, ManagedWidget, Widget, HANDLED, UNHANDLED};
use crate::window::sealed::WindowCommand;
pub type RunningWindow<'window> = kludgine::app::Window<'window, WindowCommand>;
pub type WindowAttributes = kludgine::app::WindowAttributes<WindowCommand>;
#[must_use]
pub struct Window<Behavior>
where
Behavior: WindowBehavior,
{
context: Behavior::Context,
pub attributes: WindowAttributes,
pub styles: Option<Styles>,
}
impl<Behavior> Default for Window<Behavior>
@ -40,21 +46,42 @@ where
}
}
impl Window<BoxedWidget> {
pub fn for_widget<W>(widget: W) -> Self
where
W: Widget,
{
Self::new(BoxedWidget::new(widget))
}
}
impl<Behavior> Window<Behavior>
where
Behavior: WindowBehavior,
{
pub fn new(context: Behavior::Context) -> Self {
Self {
attributes: WindowAttributes::default(),
attributes: WindowAttributes {
title: String::from("Gooey App"),
..WindowAttributes::default()
},
context,
styles: None,
}
}
pub fn styles(mut self, styles: Styles) -> Self {
self.styles = Some(styles);
self
}
pub fn run(self) -> Result<(), EventLoopError> {
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe((
self.context,
RefCell::new(Some(self.attributes)),
RefCell::new(WindowSettings {
styles: self.styles,
attributes: Some(self.attributes),
}),
)))
}
}
@ -64,7 +91,7 @@ pub trait WindowBehavior: Sized + 'static {
fn initialize(window: &mut RunningWindow<'_>, context: Self::Context) -> Self;
fn make_root(&mut self, tree: &Tree) -> ManagedWidget;
fn make_root(&mut self) -> BoxedWidget;
#[allow(unused_variables)]
fn close_requested(&self, window: &mut RunningWindow<'_>) -> bool {
@ -79,13 +106,7 @@ pub trait WindowBehavior: Sized + 'static {
}
fn run_with(context: Self::Context) -> Result<(), EventLoopError> {
GooeyWindow::<Self>::run_with(AssertUnwindSafe((
context,
RefCell::new(Some(WindowAttributes {
title: String::from("Gooey Application"),
..WindowAttributes::default()
})),
)))
Window::<Self>::new(context).run()
}
}
@ -112,7 +133,7 @@ impl<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
where
T: WindowBehavior,
{
type Context = AssertUnwindSafe<(T::Context, RefCell<Option<WindowAttributes>>)>;
type Context = AssertUnwindSafe<(T::Context, RefCell<WindowSettings>)>;
fn initialize(
mut window: RunningWindow<'_>,
@ -120,7 +141,10 @@ where
context: Self::Context,
) -> Self {
let mut behavior = T::initialize(&mut window, context.0 .0);
let root = behavior.make_root(&Tree::default());
let root = Tree::default().push_boxed(behavior.make_root(), None);
if let Some(styles) = context.0 .1.borrow_mut().styles.take() {
root.attach_styles(styles);
}
Self {
behavior,
root,
@ -152,10 +176,13 @@ where
!self.should_close
}
fn initial_window_attributes(context: &Self::Context) -> WindowAttributes {
fn initial_window_attributes(
context: &Self::Context,
) -> kludgine::app::WindowAttributes<WindowCommand> {
context
.1
.borrow_mut()
.attributes
.take()
.expect("called more than once")
}
@ -200,17 +227,18 @@ where
device_id: DeviceId,
input: KeyEvent,
is_synthetic: bool,
kludgine: &mut Kludgine,
) {
let handled = if let Some(focus) = self.root.tree.focused_widget() {
let focus = self.root.tree.widget(focus);
let mut focus = Context::new(&focus, &mut window);
recursively_handle_event(&mut focus, |widget| {
widget.keyboard_input(device_id, input.clone(), is_synthetic)
})
.is_some()
} else {
false
};
let target = self.root.tree.hovered_widget().unwrap_or(self.root.id);
let target = self.root.tree.widget(target);
let mut target = Context::new(&target, &mut window);
let handled = recursively_handle_event(&mut target, |widget| {
widget.keyboard_input(device_id, input.clone(), is_synthetic, kludgine)
})
.is_some();
drop(target);
if !handled && !input.state.is_pressed() {
match input.physical_key {
KeyCode::KeyW if window.modifiers().state().primary() => {
@ -223,6 +251,22 @@ where
}
}
fn mouse_wheel(
&mut self,
mut window: RunningWindow<'_>,
device_id: DeviceId,
delta: MouseScrollDelta,
phase: TouchPhase,
) {
let widget = self.root.tree.hovered_widget().unwrap_or(self.root.id);
let widget = self.root.tree.widget(widget);
let mut widget = Context::new(&widget, &mut window);
recursively_handle_event(&mut widget, |widget| {
widget.mouse_wheel(device_id, delta, phase)
});
}
// fn modifiers_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn ime(
@ -251,6 +295,7 @@ where
} else {
// Hover
let mut context = Context::new(&self.root, &mut window);
self.mouse_state.widget = None;
for widget in self.root.tree.widgets_at_point(location) {
let mut widget_context = context.for_other(&widget);
let relative = location
@ -266,16 +311,13 @@ where
break;
}
}
if self.mouse_state.widget.is_none() {
context.clear_hover();
}
}
}
// fn cursor_entered(
// &mut self,
// window: RunningWindow<'_>,
// device_id: DeviceId,
// ) {
// }
fn cursor_left(&mut self, mut window: RunningWindow<'_>, _device_id: DeviceId) {
if self.mouse_state.widget.take().is_some() {
let mut context = Context::new(&self.root, &mut window);
@ -283,15 +325,6 @@ where
}
}
// fn mouse_wheel(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// device_id: kludgine::app::winit::event::DeviceId,
// delta: kludgine::app::winit::event::MouseScrollDelta,
// phase: kludgine::app::winit::event::TouchPhase,
// ) {
// }
fn mouse_input(
&mut self,
mut window: RunningWindow<'_>,
@ -348,56 +381,6 @@ where
}
}
// fn touchpad_pressure(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// device_id: kludgine::app::winit::event::DeviceId,
// pressure: f32,
// stage: i64,
// ) {
// }
// fn axis_motion(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// device_id: kludgine::app::winit::event::DeviceId,
// axis: kludgine::app::winit::event::AxisId,
// value: f64,
// ) {
// }
// fn touch(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// touch: kludgine::app::winit::event::Touch,
// ) {
// }
// fn touchpad_magnify(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// device_id: kludgine::app::winit::event::DeviceId,
// delta: f64,
// phase: kludgine::app::winit::event::TouchPhase,
// ) {
// }
// fn smart_magnify(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// device_id: kludgine::app::winit::event::DeviceId,
// ) {
// }
// fn touchpad_rotate(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// device_id: kludgine::app::winit::event::DeviceId,
// delta: f32,
// phase: kludgine::app::winit::event::TouchPhase,
// ) {
// }
fn event(&mut self, event: WindowCommand, mut window: RunningWindow<'_>) {
match event {
WindowCommand::Redraw => {
@ -419,6 +402,11 @@ fn recursively_handle_event(
}
}
pub struct WindowSettings {
styles: Option<Styles>,
attributes: Option<WindowAttributes>,
}
#[derive(Default)]
struct MouseState {
location: Option<Point<Px>>,