Documentation

This commit is contained in:
Jonathan Johnson 2023-10-30 21:12:04 -07:00
parent 04e5381187
commit c9c4c9aeed
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
26 changed files with 1035 additions and 569 deletions

View file

@ -1,8 +1,8 @@
use gooey::dynamic::Dynamic;
use gooey::value::Dynamic;
use gooey::widgets::Button;
use gooey::{EventLoopError, Run};
use gooey::Run;
fn main() -> Result<(), EventLoopError> {
fn main() -> gooey::Result {
let count = Dynamic::new(0_usize);
Button::new(count.map_each(ToString::to_string))
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))

View file

@ -1,5 +1,5 @@
use gooey::widgets::Canvas;
use gooey::Run;
use gooey::{Run, Tick};
use kludgine::figures::units::Px;
use kludgine::figures::{Angle, IntoSigned, Point, Rect, Size};
use kludgine::shapes::Shape;
@ -29,6 +29,6 @@ fn main() -> gooey::Result<()> {
None,
)
})
.target_fps(60)
.tick(Tick::redraws_per_second(60))
.run()
}

View file

@ -1,27 +1,25 @@
use std::string::ToString;
use gooey::children::Children;
use gooey::dynamic::Dynamic;
use gooey::value::Dynamic;
use gooey::widgets::array::Array;
use gooey::widgets::{Button, Label};
use gooey::{EventLoopError, Run};
use gooey::{widgets, Run};
fn main() -> Result<(), EventLoopError> {
fn main() -> gooey::Result {
let counter = Dynamic::new(0i32);
let label = counter.map_each(ToString::to_string);
Array::rows(
Children::new()
.with_widget(Label::new(label))
.with_widget(Button::new("+").on_click(counter.with_clone(|counter| {
move |_| {
counter.set(counter.get() + 1);
}
})))
.with_widget(Button::new("-").on_click(counter.with_clone(|counter| {
move |_| {
counter.set(counter.get() - 1);
}
}))),
)
Array::rows(widgets![
Label::new(label),
Button::new("+").on_click(counter.with_clone(|counter| {
move |_| {
counter.set(counter.get() + 1);
}
})),
Button::new("-").on_click(counter.with_clone(|counter| {
move |_| {
counter.set(counter.get() - 1);
}
})),
])
.run()
}

View file

@ -1,6 +1,6 @@
use gooey::widgets::Input;
use gooey::{EventLoopError, Run};
use gooey::Run;
fn main() -> Result<(), EventLoopError> {
fn main() -> gooey::Result {
Input::new("Hello").run()
}

View file

@ -1,16 +1,16 @@
use gooey::children::Children;
use gooey::styles::{Styles, TextColor};
use gooey::widget::Widget;
use gooey::styles::components::TextColor;
use gooey::styles::Styles;
use gooey::widget::{Widget, Widgets};
use gooey::widgets::array::Array;
use gooey::widgets::{Button, Style};
use gooey::window::Window;
use gooey::{styles, EventLoopError, Run};
use gooey::{styles, Run};
use kludgine::Color;
fn main() -> Result<(), EventLoopError> {
fn main() -> gooey::Result {
Window::for_widget(
Array::rows(
Children::new()
Widgets::new()
.with_widget(Button::new("Default"))
.with_widget(red_text(Button::new("Styled"))),
)

View file

@ -1,4 +1,3 @@
use gooey::dynamic::Dynamic;
use gooey::kludgine::app::winit::keyboard::Key;
use gooey::kludgine::figures::units::Px;
use gooey::kludgine::figures::{Point, Rect, Size};
@ -6,9 +5,9 @@ use gooey::kludgine::render::Renderer;
use gooey::kludgine::shapes::Shape;
use gooey::kludgine::tilemap::{Object, ObjectLayer, TileKind, TileMapFocus, Tiles, TILE_SIZE};
use gooey::kludgine::Color;
use gooey::tick::Tick;
use gooey::value::Dynamic;
use gooey::widgets::TileMap;
use gooey::{EventLoopError, Run};
use gooey::{Run, Tick};
const PLAYER_SIZE: Px = Px(16);
@ -28,7 +27,7 @@ const TILES: [TileKind; 64] = {
]
};
fn main() -> Result<(), EventLoopError> {
fn main() -> gooey::Result {
let mut characters = ObjectLayer::new();
let myself = characters.push(Player {
@ -43,8 +42,7 @@ fn main() -> Result<(), EventLoopError> {
layer: 1,
id: myself,
})
.tick(Tick::fps(60, move |elapsed, input| {
// println!("Ticking {input:?}");
.tick(Tick::times_per_second(60, move |elapsed, input| {
let mut direction = Point::new(0., 0.);
if input.keys.contains(&Key::ArrowDown) {
direction.y += 1.0;

View file

@ -1,69 +0,0 @@
use std::ops::{Index, IndexMut};
use alot::OrderedLots;
use crate::widget::{BoxedWidget, MakeWidget};
#[derive(Debug, Default)]
#[must_use]
pub struct Children {
ordered: OrderedLots<BoxedWidget>,
}
impl Children {
pub const fn new() -> Self {
Self {
ordered: OrderedLots::new(),
}
}
pub fn with_widget<W>(mut self, widget: W) -> Self
where
W: MakeWidget,
{
self.ordered.push(widget.make_widget());
self
}
#[must_use]
pub fn len(&self) -> usize {
self.ordered.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ordered.is_empty()
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&BoxedWidget> {
self.ordered.get_by_index(index)
}
#[must_use]
pub fn iter(&self) -> alot::ordered::Iter<'_, BoxedWidget> {
self.into_iter()
}
}
impl Index<usize> for Children {
type Output = BoxedWidget;
fn index(&self, index: usize) -> &Self::Output {
&self.ordered[index]
}
}
impl IndexMut<usize> for Children {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.ordered[index]
}
}
impl<'a> IntoIterator for &'a Children {
type IntoIter = alot::ordered::Iter<'a, BoxedWidget>;
type Item = &'a BoxedWidget;
fn into_iter(self) -> Self::IntoIter {
self.ordered.iter()
}
}

View file

@ -1,3 +1,4 @@
//! Types that provide access to the Gooey runtime.
use std::ops::{Deref, DerefMut};
use kludgine::app::winit::event::{
@ -8,23 +9,43 @@ use kludgine::figures::{IntoSigned, Point, Rect, Size};
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::Kludgine;
use crate::dynamic::Dynamic;
use crate::graphics::Graphics;
use crate::styles::{ComponentDefaultvalue, HighlightColor, Styles};
use crate::styles::components::HighlightColor;
use crate::styles::{ComponentDefaultvalue, Styles};
use crate::value::Dynamic;
use crate::widget::{BoxedWidget, EventHandling, ManagedWidget};
use crate::window::RunningWindow;
use crate::ConstraintLimit;
/// A context to an event function.
///
/// This type is a combination of a reference to the rendering library,
/// [`Kludgine`], and a [`WidgetContext`].
pub struct EventContext<'context, 'window> {
/// The context for the widget receiving the event.
pub widget: WidgetContext<'context, 'window>,
/// The rendering library's state.
///
/// This is useful for accessing the current [scale](Kludgine::scale) or
/// information needed to measure and layout text.
pub kludgine: &'context mut Kludgine,
}
impl<'context, 'window> EventContext<'context, 'window> {
pub fn new(widget: WidgetContext<'context, 'window>, kludgine: &'context mut Kludgine) -> Self {
pub(crate) fn new(
widget: WidgetContext<'context, 'window>,
kludgine: &'context mut Kludgine,
) -> Self {
Self { widget, kludgine }
}
/// Returns a new `EventContext` with `widget` being referenced in the
/// contained [`WidgetContext`].
///
/// This function is used when one widget contains other widgets, and the
/// parent widget needs to invoke events on a child widget. This is done by
/// creating an `EventContext` pointing to the child and calling the
/// appropriate function to invoke the event.
pub fn for_other<'child>(
&'child mut self,
widget: &'child ManagedWidget,
@ -32,10 +53,14 @@ impl<'context, 'window> EventContext<'context, 'window> {
EventContext::new(self.widget.for_other(widget), self.kludgine)
}
/// Invokes [`Widget::hit_test()`](crate::widget::Widget::hit_test) on this
/// context's widget and returns the result.
pub fn hit_test(&mut self, location: Point<Px>) -> bool {
self.current_node.lock().hit_test(location, self)
}
/// Invokes [`Widget::mouse_down()`](crate::widget::Widget::mouse_down) on
/// this context's widget and returns the result.
pub fn mouse_down(
&mut self,
location: Point<Px>,
@ -47,12 +72,16 @@ impl<'context, 'window> EventContext<'context, 'window> {
.mouse_down(location, device_id, button, self)
}
/// Invokes [`Widget::hit_test()`](crate::widget::Widget::mouse_drag) on
/// this context's widget and returns the result.
pub fn mouse_drag(&mut self, location: Point<Px>, device_id: DeviceId, button: MouseButton) {
self.current_node
.lock()
.mouse_drag(location, device_id, button, self);
}
/// Invokes [`Widget::mouse_up()`](crate::widget::Widget::mouse_up) on this
/// context's widget and returns the result.
pub fn mouse_up(
&mut self,
location: Option<Point<Px>>,
@ -64,6 +93,8 @@ impl<'context, 'window> EventContext<'context, 'window> {
.mouse_up(location, device_id, button, self);
}
/// Invokes [`Widget::keyboard_input()`](crate::widget::Widget::keyboard_input) on this
/// context's widget and returns the result.
pub fn keyboard_input(
&mut self,
device_id: DeviceId,
@ -75,10 +106,14 @@ impl<'context, 'window> EventContext<'context, 'window> {
.keyboard_input(device_id, input, is_synthetic, self)
}
/// Invokes [`Widget::ime()`](crate::widget::Widget::ime) on this
/// context's widget and returns the result.
pub fn ime(&mut self, ime: Ime) -> EventHandling {
self.current_node.lock().ime(ime, self)
}
/// Invokes [`Widget::mouse_wheel()`](crate::widget::Widget::mouse_wheel) on this
/// context's widget and returns the result.
pub fn mouse_wheel(
&mut self,
device_id: DeviceId,
@ -112,6 +147,46 @@ impl<'context, 'window> EventContext<'context, 'window> {
old_hover.lock().unhover(&mut old_hover_context);
}
}
pub(crate) fn apply_pending_state(&mut self) {
let active = self.pending_state.active.take();
if self.current_node.tree.active_widget() != active.as_ref().map(|active| active.id) {
let new = match self.current_node.tree.activate(active.as_ref()) {
Ok(old) => {
if let Some(old) = old {
let mut old_context = self.for_other(&old);
old.lock().deactivate(&mut old_context);
}
true
}
Err(_) => false,
};
if new {
if let Some(active) = active {
active.lock().activate(self);
}
}
}
let focus = self.pending_state.focus.take();
if self.current_node.tree.focused_widget() != focus.as_ref().map(|focus| focus.id) {
let new = match self.current_node.tree.focus(focus.as_ref()) {
Ok(old) => {
if let Some(old) = old {
let mut old_context = self.for_other(&old);
old.lock().blur(&mut old_context);
}
true
}
Err(_) => false,
};
if new {
if let Some(focus) = focus {
focus.lock().focus(self);
}
}
}
}
}
impl<'context, 'window> Deref for EventContext<'context, 'window> {
@ -128,8 +203,11 @@ impl<'context, 'window> DerefMut for EventContext<'context, 'window> {
}
}
/// An owned `T` or an exclusive reference to a `T`.
pub enum Exclusive<'a, T> {
/// An exclusive borrow.
Borrowed(&'a mut T),
/// An owned instance.
Owned(T),
}
@ -153,12 +231,19 @@ impl<T> DerefMut for Exclusive<'_, T> {
}
}
/// A context to a function that is rendering a widget.
pub struct GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> {
/// The context of the widget being rendered.
pub widget: WidgetContext<'context, 'window>,
/// The graphics context clipped and offset to the area of the widget being
/// rendered. Drawing at 0,0 will draw at the top-left pixel of the laid-out
/// widget region.
pub graphics: Exclusive<'context, Graphics<'clip, 'gfx, 'pass>>,
}
impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> {
/// Returns a new `GraphicsContext` that allows invoking graphics functions
/// for `widget`.
pub fn for_other<'child>(
&'child mut self,
widget: &'child ManagedWidget,
@ -169,19 +254,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
}
}
pub fn measure(&mut self, available_space: Size<ConstraintLimit>) -> Size<UPx> {
self.current_node.lock().measure(available_space, self)
}
pub fn redraw(&mut self) {
// TODO this should not use clip_rect, because it forces UPx, and once
// we have scrolling, we can have negative offsets of rectangles where
// it's clipped partially.
self.current_node
.note_rendered_rect(self.graphics.clip_rect().into_signed());
self.current_node.lock().redraw(self);
}
/// Returns a new graphics context that renders to the `clip` rectangle.
pub fn clipped_to(&mut self, clip: Rect<UPx>) -> GraphicsContext<'_, 'window, '_, 'gfx, 'pass> {
GraphicsContext {
widget: self.widget.borrowed(),
@ -189,7 +262,11 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
}
}
pub fn draw_focus_ring(&mut self, styles: &Styles) {
/// Renders the default focus ring for this widget.
///
/// To ensure the correct color is used, include [`HighlightColor`] in the
/// styles request.
pub fn draw_focus_ring_using(&mut self, styles: &Styles) {
let visible_rect = Rect::from(self.graphics.size() - (UPx(1), UPx(1)));
let focus_ring = Shape::stroked_rect(
visible_rect,
@ -199,6 +276,28 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
self.graphics
.draw_shape(&focus_ring, Point::default(), None, None);
}
/// Renders the default focus ring for this widget.
pub fn draw_focus_ring(&mut self) {
self.draw_focus_ring_using(&self.query_style(&[&HighlightColor]));
}
/// Invokes [`Widget::measure()`](crate::widget::Widget::measure) on this
/// context's widget and returns the result.
pub fn measure(&mut self, available_space: Size<ConstraintLimit>) -> Size<UPx> {
self.current_node.lock().measure(available_space, self)
}
/// Invokes [`Widget::redraw()`](crate::widget::Widget::redraw) on this
/// context's widget.
pub fn redraw(&mut self) {
// TODO this should not use clip_rect, because it forces UPx, and once
// we have scrolling, we can have negative offsets of rectangles where
// it's clipped partially.
self.current_node
.note_rendered_rect(self.graphics.clip_rect().into_signed());
self.current_node.lock().redraw(self);
}
}
impl<'context, 'window, 'clip, 'gfx, 'pass> Deref
@ -219,9 +318,13 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> DerefMut
}
}
/// Converts from one context to an [`EventContext`].
pub trait AsEventContext<'window> {
/// Returns this context as an [`EventContext`].
fn as_event_context(&mut self) -> EventContext<'_, 'window>;
/// Pushes a new child widget into the widget hierarchy beneathq the
/// context's widget.
#[must_use]
fn push_child(&mut self, child: BoxedWidget) -> ManagedWidget {
let mut context = self.as_event_context();
@ -235,6 +338,7 @@ pub trait AsEventContext<'window> {
pushed_widget
}
/// Removes a widget from the hierarchy.
fn remove_child(&mut self, child: &ManagedWidget) {
let mut context = self.as_event_context();
context
@ -243,47 +347,6 @@ pub trait AsEventContext<'window> {
.remove_child(child, context.current_node);
child.lock().unmounted(&mut context.for_other(child));
}
fn apply_pending_state(&mut self) {
let mut context = self.as_event_context();
let active = context.pending_state.active.take();
if context.current_node.tree.active_widget() != active.as_ref().map(|active| active.id) {
let new = match context.current_node.tree.activate(active.as_ref()) {
Ok(old) => {
if let Some(old) = old {
let mut old_context = context.for_other(&old);
old.lock().deactivate(&mut old_context);
}
true
}
Err(_) => false,
};
if new {
if let Some(active) = active {
active.lock().activate(&mut context);
}
}
}
let focus = context.pending_state.focus.take();
if context.current_node.tree.focused_widget() != focus.as_ref().map(|focus| focus.id) {
let new = match context.current_node.tree.focus(focus.as_ref()) {
Ok(old) => {
if let Some(old) = old {
let mut old_context = context.for_other(&old);
old.lock().blur(&mut old_context);
}
true
}
Err(_) => false,
};
if new {
if let Some(focus) = focus {
focus.lock().focus(&mut context);
}
}
}
}
}
impl<'window> AsEventContext<'window> for EventContext<'_, 'window> {
@ -298,6 +361,10 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, '
}
}
/// A context for a widget.
///
/// This type provides access to the widget hierarchy from the perspective of a
/// specific widget.
pub struct WidgetContext<'context, 'window> {
current_node: &'context ManagedWidget,
window: &'context mut RunningWindow<'window>,
@ -325,6 +392,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}
}
/// Returns a new instance that borrows from `self`.
pub fn borrowed(&mut self) -> WidgetContext<'_, 'window> {
WidgetContext {
current_node: self.current_node,
@ -333,6 +401,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}
}
/// Returns a new context representing `widget`.
pub fn for_other<'child>(
&'child mut self,
widget: &'child ManagedWidget,
@ -348,15 +417,21 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
self.current_node.parent()
}
/// Ensures that this widget will be redrawn when `value` has been updated.
pub fn redraw_when_changed<T>(&self, value: &Dynamic<T>) {
value.redraw_when_changed(self.window.handle());
}
/// Returns the region that this widget last rendered at.
#[must_use]
pub fn last_rendered_at(&self) -> Option<Rect<Px>> {
self.current_node.last_rendered_at()
}
/// Sets the currently focused widget to this widget.
///
/// Widget events relating to focus changes are deferred until after the all
/// contexts for the currently firing event are dropped.
pub fn focus(&mut self) {
self.pending_state.focus = Some(self.current_node.clone());
}
@ -365,6 +440,12 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
self.pending_state.focus = None;
}
/// Clears focus from this widget, if it is the focused widget.
///
/// Returns true if this function resulted in the focus being changed.
///
/// Widget events relating to focus changes are deferred until after the all
/// contexts for the currently firing event are dropped.
pub fn blur(&mut self) -> bool {
if self.focused() {
self.clear_focus();
@ -374,6 +455,13 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}
}
/// Activates this widget, if it is not already active.
///
/// Returns true if this function resulted in the currently active widget
/// being changed.
///
/// Widget events relating to activation changes are deferred until after
/// the all contexts for the currently firing event are dropped.
pub fn activate(&mut self) -> bool {
if self
.pending_state
@ -388,6 +476,13 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}
}
/// Deactivates this widget, if it is the currently active widget.
///
/// Returns true if this function resulted in the active widget being
/// changed.
///
/// Widget events relating to activation changes are deferred until after
/// the all contexts for the currently firing event are dropped.
pub fn deactivate(&mut self) -> bool {
if self.active() {
self.clear_active();
@ -401,30 +496,48 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
self.pending_state.active = None;
}
/// Returns true if this widget is currently the active widget.
#[must_use]
pub fn active(&self) -> bool {
self.pending_state.active.as_ref() == Some(self.current_node)
}
/// Returns true if this widget is currently hovered.
#[must_use]
pub fn hovered(&self) -> bool {
self.current_node.hovered()
}
/// Returns true if this widget is currently focused for user input.
#[must_use]
pub fn focused(&self) -> bool {
self.pending_state.focus.as_ref() == Some(self.current_node)
}
/// Returns the widget this context is for.
#[must_use]
pub const fn widget(&self) -> &ManagedWidget {
self.current_node
}
/// Attaches `styles` to the widget hierarchy for this widget.
///
/// Style queries for children will return any values matching this
/// collection.
pub fn attach_styles(&self, styles: Styles) {
self.current_node.attach_styles(styles);
}
/// Queries the widget hierarchy for matching style components.
///
/// This function traverses up the widget hierarchy looking for the
/// components being requested. The resulting styles will contain the values
/// from the closest matches in the widget hierarchy.
///
/// For style components to be found, they must have previously been
/// [attached](Self::attach_styles). The [`Style`](crate::widgets::Style)
/// widget is provided as a convenient way to attach styles into the widget
/// hierarchy.
#[must_use]
pub fn query_style(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles {
self.current_node.tree.query_style(self.current_node, query)

View file

@ -2,24 +2,36 @@ use std::ops::{Deref, DerefMut};
use kludgine::figures::units::UPx;
use kludgine::figures::Rect;
use kludgine::render::Renderer;
use kludgine::ClipGuard;
/// A 2d graphics context
pub struct Graphics<'clip, 'gfx, 'pass> {
renderer: RenderContext<'clip, 'gfx, 'pass>,
}
enum RenderContext<'clip, 'gfx, 'pass> {
Renderer(kludgine::render::Renderer<'gfx, 'pass>),
Clipped(kludgine::ClipGuard<'clip, kludgine::render::Renderer<'gfx, 'pass>>),
Renderer(Renderer<'gfx, 'pass>),
Clipped(ClipGuard<'clip, Renderer<'gfx, 'pass>>),
}
impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
/// Returns a new graphics context for the given [`Renderer`].
#[must_use]
pub fn new(renderer: kludgine::render::Renderer<'gfx, 'pass>) -> Self {
pub fn new(renderer: Renderer<'gfx, 'pass>) -> Self {
Self {
renderer: RenderContext::Renderer(renderer),
}
}
/// Returns a context that has been clipped to `clip`.
///
/// The new clipping rectangle is interpreted relative to the current
/// clipping rectangle. As a side effect, this function can never expand the
/// clipping rect beyond the current clipping rect.
///
/// The returned context will report the clipped size, and all drawing
/// operations will be relative to the origin of `clip`.
pub fn clipped_to(&mut self, clip: Rect<UPx>) -> Graphics<'_, 'gfx, 'pass> {
Graphics {
renderer: RenderContext::Clipped(self.deref_mut().clipped_to(clip)),
@ -28,7 +40,7 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
}
impl<'gfx, 'pass> Deref for Graphics<'_, 'gfx, 'pass> {
type Target = kludgine::render::Renderer<'gfx, 'pass>;
type Target = Renderer<'gfx, 'pass>;
fn deref(&self) -> &Self::Target {
match &self.renderer {

View file

@ -1,35 +1,38 @@
#![warn(clippy::pedantic)]
#![allow(
clippy::module_name_repetitions,
clippy::missing_errors_doc,
clippy::missing_panics_doc
)]
//! A reactive, `wgpu`-based Graphical User Interface (GUI) crate for Rust.
#![warn(clippy::pedantic, missing_docs)]
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
pub mod children;
pub mod context;
pub mod dynamic;
pub mod graphics;
pub mod names;
mod graphics;
mod names;
pub mod styles;
pub mod tick;
mod tick;
mod tree;
mod utils;
pub mod value;
pub mod widget;
pub mod widgets;
pub mod window;
pub use kludgine;
pub use kludgine::app::winit::error::EventLoopError;
pub use kludgine::app::winit::event::ElementState;
use kludgine::app::winit::error::EventLoopError;
use kludgine::figures::units::UPx;
pub use names::Name;
pub use self::graphics::Graphics;
pub use self::tick::{InputState, Tick};
/// A limit used when measuring a widget.
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum ConstraintLimit {
/// The widget is expected to occupy a known size.
Known(UPx),
/// The widget is expected to resize itself to fit within the size provided.
ClippedAfter(UPx),
}
impl ConstraintLimit {
/// Returns the maximum measurement that will fit the constraint.
#[must_use]
pub fn max(self) -> UPx {
match self {
@ -38,8 +41,59 @@ impl ConstraintLimit {
}
}
pub type Result<T, E = EventLoopError> = std::result::Result<T, E>;
/// A result alias that defaults to the result type commonly used throughout
/// this crate.
pub type Result<T = (), E = EventLoopError> = std::result::Result<T, E>;
/// A type that can be run as an application.
pub trait Run: Sized {
fn run(self) -> Result<(), EventLoopError>;
/// Runs the provided type, returning `Ok(())` upon successful execution and
/// program exit. Note that this function may not ever return on some
/// platforms.
fn run(self) -> crate::Result;
}
/// Creates a [`Widgets`](crate::widget::Widgets) instance with the given list
/// of widgets.
#[macro_export]
macro_rules! widgets {
() => {
$crate::widget::Widgets::new()
};
($($widget:expr),+) => {{
let mut widgets = $crate::widget::Widgets::with_capacity($crate::count!($($widget),+ ;));
$(widgets.push($widget);)+
widgets
}};
($($widget:expr),+ ,) => {{
$crate::widgets!($($widget),+)
}};
}
#[macro_export]
#[doc(hidden)]
macro_rules! count {
($value:expr ;) => {
1
};
($value:expr , $($remaining:expr),+ ;) => {
1 + $crate::count!($($remaining),+ ;)
}
}
/// Creates a [`Styles`](crate::styles::Styles) instance with the given
/// name/component pairs.
#[macro_export]
macro_rules! styles {
() => {{
$crate::styles::Styles::new()
}};
($($component:expr => $value:expr),*) => {{
let mut styles = $crate::styles::Styles::with_capacity($crate::count!($($value),* ;));
$(styles.insert(&$component, $value);)*
styles
}};
($($component:expr => $value:expr),* ,) => {{
$crate::styles!($($component => $value),*)
}};
}

View file

@ -5,10 +5,17 @@ use interner::global::{GlobalString, StringPool};
static NAMES: StringPool = StringPool::new();
/// A smart-string type that is used as a "name" in Gooey.
///
/// This type ensures that globably only one instance of any unique wrapped
/// string exists. By ensuring all instances of each unique string are the same
/// exact underlying instance, optimizations can be made that avoid string
/// comparisons.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Name(GlobalString);
impl Name {
/// Returns a name for the given string.
pub fn new<'a>(name: impl Into<Cow<'a, str>>) -> Self {
Self(NAMES.get(name))
}

View file

@ -1,3 +1,5 @@
//! Types for styling widgets.
use std::borrow::Cow;
use std::collections::{hash_map, HashMap};
use std::sync::Arc;
@ -5,64 +7,48 @@ use std::sync::Arc;
use crate::names::Name;
use crate::utils::Lazy;
#[macro_export]
#[doc(hidden)]
macro_rules! count {
($value:expr ;) => {
1
};
($value:expr , $($remaining:expr),+ ;) => {
1 + count!($($remaining),+ ;)
}
}
#[macro_export]
macro_rules! styles {
() => {{
$crate::styles::Styles::new()
}};
($($component:expr => $value:expr),*) => {{
let mut styles = $crate::styles::Styles::with_capacity($crate::count!($($value),* ;));
$(styles.push(&$component, $value);)*
styles
}};
($($component:expr => $value:expr),* ,) => {{
$crate::styles!($($component => $value),*)
}};
}
pub mod components;
/// A collection of style components organized by their name.
#[derive(Clone, Debug, Default)]
pub struct Styles(Arc<HashMap<Group, HashMap<Name, Component>>>);
impl Styles {
/// Returns an empty collection.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Returns a collection with the capacity to hold up to `capacity` elements
/// without reallocating.
#[must_use]
pub fn with_capacity(components: usize) -> Self {
Self(Arc::new(HashMap::with_capacity(components)))
pub fn with_capacity(capacity: usize) -> Self {
Self(Arc::new(HashMap::with_capacity(capacity)))
}
pub fn push_component(&mut self, name: ComponentName, component: impl Into<Component>) {
/// Inserts a [`Component`] with a given name.
pub fn insert_named(&mut self, name: ComponentName, component: impl Into<Component>) {
Arc::make_mut(&mut self.0)
.entry(name.group)
.or_default()
.insert(name.name, component.into());
}
pub fn push(&mut self, name: &impl NamedComponent, component: impl Into<Component>) {
/// Inserts a [`Component`] using then name provided.
pub fn insert(&mut self, name: &impl NamedComponent, component: impl Into<Component>) {
let name = name.name().into_owned();
self.push_component(name, component);
self.insert_named(name, component);
}
/// Adds a [`Component`] for the name provided and returns self.
#[must_use]
pub fn with(mut self, name: &impl NamedComponent, component: impl Into<Component>) -> Self {
self.push(name, component);
self.insert(name, component);
self
}
/// Returns the associated component for the given name, if found.
#[must_use]
pub fn get<Named>(&self, component: &Named) -> Option<&Component>
where
@ -74,6 +60,8 @@ impl Styles {
.and_then(|group| group.get(&name.name))
}
/// Returns the component associated with the given name, or if not found,
/// returns the default value provided by the definition.
#[must_use]
pub fn get_or_default<Named>(&self, component: &Named) -> Named::ComponentType
where
@ -93,7 +81,7 @@ impl FromIterator<(ComponentName, Component)> for Styles {
let iter = iter.into_iter();
let mut styles = Self::with_capacity(iter.size_hint().0);
for (name, component) in iter {
styles.push_component(name, component);
styles.insert_named(name, component);
}
styles
}
@ -113,6 +101,7 @@ impl IntoIterator for Styles {
}
}
/// An iterator over the owned contents of a [`Styles`] instance.
pub struct StylesIntoIter {
main: hash_map::IntoIter<Group, HashMap<Name, Component>>,
names: Option<(Group, hash_map::IntoIter<Name, Component>)>,
@ -136,7 +125,6 @@ impl Iterator for StylesIntoIter {
}
}
pub type StyleQuery = Vec<ComponentName>;
use std::any::Any;
use std::fmt::Debug;
use std::panic::{RefUnwindSafe, UnwindSafe};
@ -145,12 +133,17 @@ use kludgine::figures::units::{Lp, Px};
use kludgine::figures::ScreenScale;
use kludgine::Color;
/// A value of a style component.
#[derive(Debug, Clone)]
pub enum Component {
/// A color.
Color(Color),
/// A single-dimension measurement.
Dimension(Dimension),
/// A percentage between 0.0 and 1.0.
Percent(f32),
Boxed(BoxedComponent),
/// A custom component type.
Boxed(CustomComponent),
}
impl From<Color> for Component {
@ -221,9 +214,12 @@ impl TryFrom<Component> for Lp {
}
}
/// A 1-dimensional measurement.
#[derive(Debug, Clone, Copy)]
pub enum Dimension {
/// Physical Pixels
Px(Px),
/// Logical Pixels
Lp(Lp),
}
@ -266,10 +262,12 @@ impl ScreenScale for Dimension {
}
}
/// A custom component value.
#[derive(Debug, Clone)]
pub struct BoxedComponent(Arc<dyn AnyComponent>);
pub struct CustomComponent(Arc<dyn AnyComponent>);
impl BoxedComponent {
impl CustomComponent {
/// Wraps an arbitrary value so that it can be used as a [`Component`].
pub fn new<T>(value: T) -> Self
where
T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static,
@ -277,6 +275,8 @@ impl BoxedComponent {
Self(Arc::new(value))
}
/// Return the contained value cast as `T`. Returns `None` if `T` does is
/// not the same type that was provided when this component was created.
#[must_use]
pub fn downcast<T>(&self) -> Option<&T>
where
@ -299,10 +299,12 @@ where
}
}
/// A style component group.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Group(Name);
impl Group {
/// Returns a new instance using the group name of `T`.
#[must_use]
pub fn new<T>() -> Self
where
@ -311,6 +313,7 @@ impl Group {
Self(T::name())
}
/// Returns true if this instance matches the group name of `T`.
#[must_use]
pub fn matches<T>(&self) -> bool
where
@ -320,10 +323,13 @@ impl Group {
}
}
/// A type that represents a group of style components.
pub trait ComponentGroup {
/// Returns the name of the group.
fn name() -> Name;
}
/// The Global style components group.
pub enum Global {}
impl ComponentGroup for Global {
@ -332,13 +338,17 @@ impl ComponentGroup for Global {
}
}
/// A fully-qualified style component name.
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct ComponentName {
/// The group name.
pub group: Group,
/// The name of the component within the group.
pub name: Name,
}
impl ComponentName {
/// Returns a new instance using `group` and `name`.
pub fn new(group: Group, name: impl Into<Name>) -> Self {
Self {
group,
@ -346,6 +356,7 @@ impl ComponentName {
}
}
/// Returns a new instance using `G` and `name`.
pub fn named<G: ComponentGroup>(name: impl Into<Name>) -> Self {
Self::new(Group::new::<G>(), name)
}
@ -356,17 +367,26 @@ impl From<&'static Lazy<ComponentName>> for ComponentName {
(**value).clone()
}
}
/// A type that represents a named style component.
pub trait NamedComponent {
/// Returns the name of the style component.
fn name(&self) -> Cow<'_, ComponentName>;
}
/// A type that represents a named component with a default value of a specific
/// Rust type.
pub trait ComponentDefinition: NamedComponent {
/// The type that will be contained in the [`Component`].
type ComponentType: Into<Component> + TryFrom<Component, Error = Component>;
/// Returns the default value to use for this component.
fn default_value(&self) -> Self::ComponentType;
}
/// A type that represents a named component with a default value.
pub trait ComponentDefaultvalue: NamedComponent {
/// Returns the default value for this component.
fn default_component_value(&self) -> Component;
}
@ -390,71 +410,3 @@ impl NamedComponent for Cow<'_, ComponentName> {
Cow::Borrowed(self)
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct TextSize;
impl NamedComponent for TextSize {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("text_size"))
}
}
impl ComponentDefinition for TextSize {
type ComponentType = Dimension;
fn default_value(&self) -> Dimension {
Dimension::Lp(Lp::points(12))
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct LineHeight;
impl NamedComponent for LineHeight {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("line_height"))
}
}
impl ComponentDefinition for LineHeight {
type ComponentType = Dimension;
fn default_value(&self) -> Dimension {
Dimension::Lp(Lp::points(14))
}
}
#[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
}
}

79
src/styles/components.rs Normal file
View file

@ -0,0 +1,79 @@
//! All style components supported by the built-in widgets.
use std::borrow::Cow;
use kludgine::figures::units::Lp;
use kludgine::Color;
use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent};
/// The [`Dimension`] to use as the size to render text.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct TextSize;
impl NamedComponent for TextSize {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("text_size"))
}
}
impl ComponentDefinition for TextSize {
type ComponentType = Dimension;
fn default_value(&self) -> Dimension {
Dimension::Lp(Lp::points(12))
}
}
/// The [`Dimension`] to use to space multiple lines of text.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct LineHeight;
impl NamedComponent for LineHeight {
fn name(&self) -> Cow<'_, ComponentName> {
Cow::Owned(ComponentName::named::<Global>("line_height"))
}
}
impl ComponentDefinition for LineHeight {
type ComponentType = Dimension;
fn default_value(&self) -> Dimension {
Dimension::Lp(Lp::points(14))
}
}
/// The [`Color`] to use when rendering text.
#[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
}
}
/// A [`Color`] to be used as a highlight color.
#[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

@ -6,8 +6,12 @@ use std::time::{Duration, Instant};
use kludgine::app::winit::event::KeyEvent;
use kludgine::app::winit::keyboard::Key;
use crate::widget::{EventHandling, HANDLED, UNHANDLED};
use crate::context::WidgetContext;
use crate::value::Dynamic;
use crate::widget::{EventHandling, HANDLED, IGNORED};
/// A fixed-rate callback that provides access to tracked input on its
/// associated widget.
#[derive(Clone, Debug)]
#[must_use]
pub struct Tick {
@ -16,12 +20,18 @@ pub struct Tick {
}
impl Tick {
pub fn rendered(&self) {
self.data.rendered_frame.fetch_add(1, Ordering::AcqRel);
/// Signals that this widget has been redrawn.
pub fn rendered(&self, context: &WidgetContext<'_, '_>) {
context.redraw_when_changed(&self.data.tick_number);
self.data.sync.notify_one();
}
/// Processes `input`.
///
/// If the event matches a key that has been marked as
/// handled, [`HANDLED`] will be returned. Otherwise, [`UNHANDLED`] will be
/// returned,
#[must_use]
pub fn key_input(&self, input: &KeyEvent) -> EventHandling {
let mut state = self.data.state();
@ -35,13 +45,71 @@ impl Tick {
if self.handled_keys.contains(&input.logical_key) {
HANDLED
} else {
UNHANDLED
IGNORED
}
}
/// Returns a new tick that invokes `tick`, aiming to repeat at the given
/// duration.
pub fn new<F>(tick_every: Duration, tick: F) -> Self
where
F: FnMut(Duration, &InputState) + Send + 'static,
{
let now = Instant::now();
let data = Arc::new(TickData {
state: Mutex::new(TickState {
last_time: now,
next_target: now,
keep_running: true,
frame: 0,
input: InputState::default(),
}),
period: tick_every,
sync: Condvar::new(),
rendered_frame: AtomicUsize::new(0),
tick_number: Dynamic::default(),
});
std::thread::spawn({
let data = data.clone();
move || tick_loop(&data, tick)
});
Self {
data,
handled_keys: HashSet::new(),
}
}
/// Returns a new tick that invokes `tick` at a target number of times per
/// second.
pub fn times_per_second<F>(times_per_second: u32, tick: F) -> Self
where
F: FnMut(Duration, &InputState) + Send + 'static,
{
Self::new(Duration::from_secs(1) / times_per_second, tick)
}
/// Returns a new tick that redraws its associated widget at a target rate
/// of `x times_per_second`.
pub fn redraws_per_second(times_per_second: u32) -> Self {
Self::times_per_second(times_per_second, |_, _| {})
}
/// Adds the collection of [`Key`]s to the list that are handled, and
/// returns self.
///
/// The list of keys provided will be prevented from propagating.
pub fn handled_keys(mut self, keys: impl IntoIterator<Item = Key>) -> Self {
self.handled_keys.extend(keys);
self
}
}
/// The current state of input during the execution of a [`Tick`].
#[derive(Default, Debug)]
pub struct WatchedInput {
pub struct InputState {
/// A collection of all keys currently pressed.
pub keys: HashSet<Key>,
}
@ -51,6 +119,7 @@ struct TickData {
period: Duration,
sync: Condvar,
rendered_frame: AtomicUsize,
tick_number: Dynamic<u64>,
}
impl TickData {
@ -67,55 +136,12 @@ struct TickState {
next_target: Instant,
keep_running: bool,
frame: usize,
input: WatchedInput,
}
impl Tick {
pub fn new<F>(tick_every: Duration, tick: F) -> Self
where
F: FnMut(Duration, &WatchedInput) + Send + 'static,
{
let now = Instant::now();
let data = Arc::new(TickData {
state: Mutex::new(TickState {
last_time: now,
next_target: now,
keep_running: true,
frame: 0,
input: WatchedInput::default(),
}),
period: tick_every,
sync: Condvar::new(),
rendered_frame: AtomicUsize::new(0),
});
std::thread::spawn({
let data = data.clone();
move || tick_loop(&data, tick)
});
Self {
data,
handled_keys: HashSet::new(),
}
}
pub fn fps<F>(frames_per_second: u32, tick: F) -> Self
where
F: FnMut(Duration, &WatchedInput) + Send + 'static,
{
Self::new(Duration::from_secs(1) / frames_per_second, tick)
}
pub fn handled_keys(mut self, keys: impl IntoIterator<Item = Key>) -> Self {
self.handled_keys.extend(keys);
self
}
input: InputState,
}
fn tick_loop<F>(data: &TickData, mut tick: F)
where
F: FnMut(Duration, &WatchedInput),
F: FnMut(Duration, &InputState),
{
let mut state = data.state();
while state.keep_running {
@ -135,14 +161,15 @@ where
.checked_duration_since(state.last_time)
.expect("instant never decreases");
state.frame += 1;
// TODO we need a way to batch updates for a context so that during a
// tick, no changed values trigger a redraw until we are done with the
// tick. Otherwise, a frame may start being rendered while we're still
// evaluating the tick since it's in its own thread.
tick(elapsed, &state.input);
state.next_target = (state.next_target + data.period).max(now);
state.last_time = now;
// Signal that we have a new frame, which will cause the widget to
// redraw.
data.tick_number.map_mut(|tick| *tick += 1);
// Wait for a frame to be rendered.
while state.keep_running {
let current_frame = data.rendered_frame.load(Ordering::Acquire);

View file

@ -203,7 +203,7 @@ impl TreeData {
if let Some(styles) = &node.styles {
query.retain(|name| {
if let Some(component) = styles.get(name) {
resolved.push(name, component.clone());
resolved.insert(name, component.clone());
false
} else {
true

View file

@ -1,21 +1,27 @@
//! Types for storing and interacting with values in Widgets.
use std::fmt::Debug;
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use kludgine::app::WindowHandle;
use crate::context::WidgetContext;
use crate::window::sealed::WindowCommand;
/// An instance of a value that provides APIs to observe and react to its
/// contents.
#[derive(Debug)]
pub struct Dynamic<T>(Arc<DynamicData<T>>);
impl<T> Dynamic<T> {
/// Creates a new instance wrapping `value`.
pub fn new(value: T) -> Self {
Self(Arc::new(DynamicData {
state: Mutex::new(State {
wrapped: GenerationalValue {
value,
generation: 0,
generation: Generation::default(),
},
callbacks: Vec::new(),
windows: Vec::new(),
@ -25,22 +31,30 @@ impl<T> Dynamic<T> {
}))
}
/// Maps the contents with read-only access.
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
let state = self.state();
map(&state.wrapped.value)
}
/// Maps the contents with exclusive access. Before returning from this
/// function, all observers will be notified that the contents have been
/// updated.
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
self.0.map_mut(map)
}
pub fn for_each<F>(&self, mut map: F)
/// Attaches `for_each` to this value so that it is invoked each time the
/// value's contents are updated.
pub fn for_each<F>(&self, mut for_each: F)
where
F: for<'a> FnMut(&'a T) + Send + 'static,
{
self.0.for_each(move |gen| map(&gen.value));
self.0.for_each(move |gen| for_each(&gen.value));
}
/// Creates a new dynamic value that contains the result of invoking `map`
/// each time this value is changed.
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
@ -49,14 +63,38 @@ impl<T> Dynamic<T> {
self.0.map_each(move |gen| map(&gen.value))
}
/// A helper function that invokes `with_clone` with a clone of self. This
/// code may produce slightly more readable code.
///
/// ```rust
/// let value = gooey::dynamic::Dynamic::new(1);
///
/// // Using with_clone
/// value.with_clone(|value| {
/// std::thread::spawn(move || {
/// println!("{}", value.get());
/// })
/// });
///
/// // Using an explicit clone
/// std::thread::spawn({
/// let value = value.clone();
/// move || {
/// println!("{}", value.get());
/// }
/// });
///
/// println!("{}", value.get());
/// ````
pub fn with_clone<R>(&self, with_clone: impl FnOnce(Self) -> R) -> R {
with_clone(self.clone())
}
pub fn redraw_when_changed(&self, window: WindowHandle<WindowCommand>) {
pub(crate) fn redraw_when_changed(&self, window: WindowHandle<WindowCommand>) {
self.0.redraw_when_changed(window);
}
/// Returns a clone of the currently contained value.
#[must_use]
pub fn get(&self) -> T
where
@ -65,19 +103,25 @@ impl<T> Dynamic<T> {
self.0.get().value
}
/// Replaces the contents with `new_value`, returning the previous contents.
/// Before returning from this function, all observers will be notified that
/// the contents have been updated.
#[must_use]
pub fn replace(&self, new_value: T) -> T {
self.0.map_mut(|value| std::mem::replace(value, new_value))
}
/// Stores `new_value` in this dynamic. Before returning from this function,
/// all observers will be notified that the contents have been updated.
pub fn set(&self, new_value: T) {
let _old = self.replace(new_value);
}
/// Returns a new reference-based reader for this dynamic value.
#[must_use]
pub fn create_ref_reader(&self) -> DynamicRefReader<T> {
pub fn create_ref_reader(&self) -> DynamicReader<T> {
self.state().readers += 1;
DynamicRefReader {
DynamicReader {
source: self.0.clone(),
read_generation: self.0.state().wrapped.generation,
}
@ -87,12 +131,22 @@ impl<T> Dynamic<T> {
self.0.state()
}
/// Returns the current generation of the value.
#[must_use]
pub fn generation(&self) -> usize {
pub fn generation(&self) -> Generation {
self.state().wrapped.generation
}
}
impl<T> Default for Dynamic<T>
where
T: Default,
{
fn default() -> Self {
Self::new(T::default())
}
}
impl<T> Clone for Dynamic<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
@ -109,7 +163,7 @@ impl<T> Drop for Dynamic<T> {
}
}
impl<T> From<Dynamic<T>> for DynamicRefReader<T> {
impl<T> From<Dynamic<T>> for DynamicReader<T> {
fn from(value: Dynamic<T>) -> Self {
value.create_ref_reader()
}
@ -149,7 +203,7 @@ impl<T> DynamicData<T> {
let mut state = self.state();
let old = {
let state = &mut *state;
let generation = state.wrapped.generation.wrapping_add(1);
let generation = state.wrapped.generation.next();
let result = map(&mut state.wrapped.value);
state.wrapped.generation = generation;
@ -227,32 +281,45 @@ where
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GenerationalValue<T> {
struct GenerationalValue<T> {
pub value: T,
pub generation: usize,
pub generation: Generation,
}
/// A reader that tracks the last generation accessed through this reader.
#[derive(Debug)]
pub struct DynamicRefReader<T> {
pub struct DynamicReader<T> {
source: Arc<DynamicData<T>>,
read_generation: usize,
read_generation: Generation,
}
impl<T> DynamicRefReader<T> {
impl<T> DynamicReader<T> {
/// Maps the contents of the dynamic value and returns the result.
///
/// This function marks the currently stored value as being read.
pub fn map_ref<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
let state = self.source.state();
self.read_generation = state.wrapped.generation;
map(&state.wrapped.value)
}
/// Returns a clone of the currently contained value.
///
/// This function marks the currently stored value as being read.
#[must_use]
pub fn get(&self) -> T
pub fn get(&mut self) -> T
where
T: Clone,
{
self.source.get().value
let GenerationalValue { value, generation } = self.source.get();
self.read_generation = generation;
value
}
/// Blocks the current thread until the contained value has been updated or
/// there are no remaining writers for the value.
///
/// Returns true if a newly updated value was discovered.
pub fn block_until_updated(&mut self) -> bool {
let mut state = self.source.state();
loop {
@ -269,13 +336,9 @@ impl<T> DynamicRefReader<T> {
.map_or_else(PoisonError::into_inner, |g| g);
}
}
pub fn redraw_if_changed(&mut self, window: WindowHandle<WindowCommand>) {
self.source.redraw_when_changed(window);
}
}
impl<T> Clone for DynamicRefReader<T> {
impl<T> Clone for DynamicReader<T> {
fn clone(&self) -> Self {
self.source.state().readers += 1;
Self {
@ -285,7 +348,7 @@ impl<T> Clone for DynamicRefReader<T> {
}
}
impl<T> Drop for DynamicRefReader<T> {
impl<T> Drop for DynamicReader<T> {
fn drop(&mut self) {
let mut state = self.source.state();
state.readers -= 1;
@ -299,3 +362,103 @@ fn disconnecting_reader_from_dynamic() {
drop(value);
assert!(!ref_reader.block_until_updated());
}
/// A tag that represents an individual revision of a [`Dynamic`] value.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub struct Generation(usize);
impl Generation {
/// Returns the next tag.
#[must_use]
pub fn next(self) -> Self {
Self(self.0.wrapping_add(1))
}
}
/// A value that may be either constant or dynamic.
#[derive(Debug)]
pub enum Value<T> {
/// A value that will not ever change externally.
Constant(T),
/// A value that may be updated externally.
Dynamic(Dynamic<T>),
}
impl<T> Value<T> {
/// Returns a [`Value::Dynamic`] containing `value`.
pub fn dynamic(value: T) -> Self {
Self::Dynamic(Dynamic::new(value))
}
/// Maps the current contents to `map` and returns the result.
pub fn map<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
match self {
Value::Constant(value) => map(value),
Value::Dynamic(dynamic) => dynamic.map_ref(map),
}
}
/// Maps the current contents with exclusive access and returns the result.
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
match self {
Value::Constant(value) => map(value),
Value::Dynamic(dynamic) => dynamic.map_mut(map),
}
}
/// Returns a clone of the currently stored value.
pub fn get(&mut self) -> T
where
T: Clone,
{
self.map(Clone::clone)
}
/// Returns the current generation of the data stored, if the contained
/// value is [`Dynamic`].
pub fn generation(&self) -> Option<Generation> {
match self {
Value::Constant(_) => None,
Value::Dynamic(value) => Some(value.generation()),
}
}
/// Marks the widget for redraw when this value is updated.
///
/// This function has no effect if the value is constant.
pub fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) {
if let Value::Dynamic(dynamic) = self {
context.redraw_when_changed(dynamic);
}
}
}
/// A type that can be converted into a [`Value`].
pub trait IntoValue<T> {
/// Returns this type as a [`Value`].
fn into_value(self) -> Value<T>;
}
impl<T> IntoValue<T> for T {
fn into_value(self) -> Value<T> {
Value::Constant(self)
}
}
impl<'a> IntoValue<String> for &'a str {
fn into_value(self) -> Value<String> {
Value::Constant(self.to_owned())
}
}
impl<T> IntoValue<T> for Dynamic<T> {
fn into_value(self) -> Value<T> {
Value::Dynamic(self)
}
}
impl<T> IntoValue<T> for Value<T> {
fn into_value(self) -> Value<T> {
self
}
}

View file

@ -1,61 +1,83 @@
//! Types for creating reusable widgets (aka components or views).
use std::clone::Clone;
use std::fmt::Debug;
use std::ops::ControlFlow;
use std::ops::{ControlFlow, Deref};
use std::panic::UnwindSafe;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use kludgine::app::winit::error::EventLoopError;
use kludgine::app::winit::event::{
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{Point, Rect, Size};
use crate::context::{EventContext, GraphicsContext, WidgetContext};
use crate::dynamic::Dynamic;
use crate::styles::{Component, Group, Styles};
use crate::context::{EventContext, GraphicsContext};
use crate::styles::Styles;
use crate::tree::{Tree, WidgetId};
use crate::widgets::Style;
use crate::window::{RunningWindow, Window, WindowBehavior};
use crate::{ConstraintLimit, Run};
/// A type that makes up a graphical user interface.
///
/// This type can go by many names in other UI frameworks: View, Component,
/// Control.
pub trait Widget: Send + UnwindSafe + Debug + 'static {
/// Redraw the contents of this widget.
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>);
/// Measure this widget and returns the ideal size based on its contents and
/// the `available_space`.
fn measure(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
) -> Size<UPx>;
/// The widget has been mounted into a parent widget.
#[allow(unused_variables)]
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {}
/// The widget has been removed from its parent widget.
#[allow(unused_variables)]
fn unmounted(&mut self, context: &mut EventContext<'_, '_>) {}
/// Returns true if this widget should respond to mouse input at `location`.
#[allow(unused_variables)]
fn hit_test(&mut self, location: Point<Px>, context: &mut EventContext<'_, '_>) -> bool {
false
}
/// The widget is currently has a cursor hovering it at `location`.
#[allow(unused_variables)]
fn hover(&mut self, location: Point<Px>, context: &mut EventContext<'_, '_>) {}
/// The widget is no longer being hovered.
#[allow(unused_variables)]
fn unhover(&mut self, context: &mut EventContext<'_, '_>) {}
/// The widget has received focus for user input.
#[allow(unused_variables)]
fn focus(&mut self, context: &mut EventContext<'_, '_>) {}
/// The widget is no longer focused for user input.
#[allow(unused_variables)]
fn blur(&mut self, context: &mut EventContext<'_, '_>) {}
/// The widget has become the active widget.
#[allow(unused_variables)]
fn activate(&mut self, context: &mut EventContext<'_, '_>) {}
/// The widget is no longer active.
#[allow(unused_variables)]
fn deactivate(&mut self, context: &mut EventContext<'_, '_>) {}
/// A mouse button event has occurred at `location`. Returns whether the
/// event has been handled or not.
///
/// If an event is handled, the widget will receive callbacks for
/// [`mouse_drag`](Self::mouse_drag) and [`mouse_up`](Self::mouse_up).
#[allow(unused_variables)]
fn mouse_down(
&mut self,
@ -64,9 +86,11 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
button: MouseButton,
context: &mut EventContext<'_, '_>,
) -> EventHandling {
UNHANDLED
IGNORED
}
/// A mouse button is being held down as the cursor is moved across the
/// widget.
#[allow(unused_variables)]
fn mouse_drag(
&mut self,
@ -77,6 +101,7 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
) {
}
/// A mouse button is no longer being pressed.
#[allow(unused_variables)]
fn mouse_up(
&mut self,
@ -87,6 +112,8 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
) {
}
/// A keyboard event has been sent to this widget. Returns whether the event
/// has been handled or not.
#[allow(unused_variables)]
fn keyboard_input(
&mut self,
@ -95,13 +122,18 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
is_synthetic: bool,
context: &mut EventContext<'_, '_>,
) -> EventHandling {
UNHANDLED
}
#[allow(unused_variables)]
fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling {
UNHANDLED
IGNORED
}
/// An input manager event has been sent to this widget. Returns whether the
/// event has been handled or not.
#[allow(unused_variables)]
fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling {
IGNORED
}
/// A mouse wheel event has been sent to this widget. Returns whether the
/// event has been handled or not.
#[allow(unused_variables)]
fn mouse_wheel(
&mut self,
@ -110,14 +142,17 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
phase: TouchPhase,
context: &mut EventContext<'_, '_>,
) -> EventHandling {
UNHANDLED
IGNORED
}
#[allow(unused_variables)]
fn query_component(&self, group: Group, name: &str) -> Option<Component> {
None
}
// #[allow(unused_variables)]
// fn query_component(&self, group: Group, name: &str) -> Option<Component> {
// None
// }
/// Associates `styles` with this widget.
///
/// This is equivalent to `Style::new(styles, self)`.
fn with_styles(self, styles: impl Into<Styles>) -> Style
where
Self: Sized,
@ -130,15 +165,18 @@ impl<T> Run for T
where
T: Widget,
{
fn run(self) -> crate::Result<(), EventLoopError> {
fn run(self) -> crate::Result {
BoxedWidget::new(self).run()
}
}
/// A type that can create a widget.
pub trait MakeWidget: Sized {
/// Returns a new widget.
fn make_widget(self) -> BoxedWidget;
fn run(self) -> Result<(), EventLoopError> {
/// Runs the widget this type creates as an application.
fn run(self) -> crate::Result {
self.make_widget().run()
}
}
@ -152,20 +190,29 @@ where
}
}
/// A type that represents whether an event has been handled or ignored.
pub type EventHandling = ControlFlow<EventHandled, EventIgnored>;
/// A marker type that represents a handled event.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct EventHandled;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
/// A marker type that represents an ignored event.
pub struct EventIgnored;
/// An [`EventHandling`] value that represents a handled event.
pub const HANDLED: EventHandling = EventHandling::Break(EventHandled);
pub const UNHANDLED: EventHandling = EventHandling::Continue(EventIgnored);
/// An [`EventHandling`] value that represents an ignored event.
pub const IGNORED: EventHandling = EventHandling::Continue(EventIgnored);
/// An instance of a [`Widget`].
#[derive(Clone, Debug)]
pub struct BoxedWidget(Arc<Mutex<dyn Widget>>);
impl BoxedWidget {
/// Returns a new instance containing `widget`.
pub fn new<W>(widget: W) -> Self
where
W: Widget,
@ -179,7 +226,7 @@ impl BoxedWidget {
}
impl Run for BoxedWidget {
fn run(self) -> crate::Result<(), EventLoopError> {
fn run(self) -> crate::Result {
Window::<BoxedWidget>::new(self).run()
}
}
@ -204,84 +251,9 @@ impl WindowBehavior for BoxedWidget {
}
}
#[derive(Debug)]
pub enum Value<T> {
Constant(T),
Dynamic(Dynamic<T>),
}
impl<T> Value<T> {
pub fn dynamic(value: T) -> Self {
Self::Dynamic(Dynamic::new(value))
}
pub fn constant(value: T) -> Self {
Self::Constant(value)
}
pub fn map<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
match self {
Value::Constant(value) => map(value),
Value::Dynamic(dynamic) => dynamic.map_ref(map),
}
}
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
match self {
Value::Constant(value) => map(value),
Value::Dynamic(dynamic) => dynamic.map_mut(map),
}
}
pub fn get(&mut self) -> T
where
T: Clone,
{
self.map(Clone::clone)
}
pub fn generation(&self) -> Option<usize> {
match self {
Value::Constant(_) => None,
Value::Dynamic(value) => Some(value.generation()),
}
}
pub fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) {
if let Value::Dynamic(dynamic) = self {
context.redraw_when_changed(dynamic);
}
}
}
pub trait IntoValue<T> {
fn into_value(self) -> Value<T>;
}
impl<T> IntoValue<T> for T {
fn into_value(self) -> Value<T> {
Value::Constant(self)
}
}
impl<'a> IntoValue<String> for &'a str {
fn into_value(self) -> Value<String> {
Value::Constant(self.to_owned())
}
}
impl<T> IntoValue<T> for Dynamic<T> {
fn into_value(self) -> Value<T> {
Value::Dynamic(self)
}
}
impl<T> IntoValue<T> for Value<T> {
fn into_value(self) -> Value<T> {
self
}
}
/// A function that can be invoked with a parameter (`T`) and returns `R`.
///
/// This type is used by widgets to signal various events.
pub struct Callback<T = (), R = ()>(Box<dyn CallbackFunction<T, R>>);
impl<T, R> Debug for Callback<T, R> {
@ -293,6 +265,8 @@ impl<T, R> Debug for Callback<T, R> {
}
impl<T, R> Callback<T, R> {
/// Returns a new instance that calls `function` each time the callback is
/// invoked.
pub fn new<F>(function: F) -> Self
where
F: FnMut(T) -> R + Send + UnwindSafe + 'static,
@ -300,6 +274,7 @@ impl<T, R> Callback<T, R> {
Self(Box::new(function))
}
/// Invokes the wrapped function and returns the produced value.
pub fn invoke(&mut self, value: T) -> R {
self.0.invoke(value)
}
@ -318,6 +293,7 @@ where
}
}
/// A [`Widget`] that has been attached to a widget hierarchy.
#[derive(Clone)]
pub struct ManagedWidget {
pub(crate) id: WidgetId,
@ -343,26 +319,31 @@ impl ManagedWidget {
self.tree.note_rendered_rect(self.id, rect);
}
/// Returns the region that the widget was last rendered at.
#[must_use]
pub fn last_rendered_at(&self) -> Option<Rect<Px>> {
self.tree.last_rendered_at(self.id)
}
/// Returns true if this widget is the currently active widget.
#[must_use]
pub fn active(&self) -> bool {
self.tree.active_widget() == Some(self.id)
}
/// Returns true if this widget is currently the hovered widget.
#[must_use]
pub fn hovered(&self) -> bool {
self.tree.hovered_widget() == Some(self.id)
}
/// Returns true if this widget is the currently focused widget.
#[must_use]
pub fn focused(&self) -> bool {
self.tree.focused_widget() == Some(self.id)
}
/// Returns the parent of this widget.
#[must_use]
pub fn parent(&self) -> Option<ManagedWidget> {
self.tree.parent(self.id).map(|id| self.tree.widget(id))
@ -384,3 +365,75 @@ impl PartialEq<BoxedWidget> for ManagedWidget {
&self.widget == other
}
}
/// A list of [`Widget`]s.
#[derive(Debug, Default)]
#[must_use]
pub struct Widgets {
ordered: Vec<BoxedWidget>,
}
impl Widgets {
/// Returns an empty list.
pub const fn new() -> Self {
Self {
ordered: Vec::new(),
}
}
/// Returns a list with enough capacity to hold `capacity` widgets without
/// reallocation.
pub fn with_capacity(capacity: usize) -> Self {
Self {
ordered: Vec::with_capacity(capacity),
}
}
/// Pushes `widget` into the list.
pub fn push<W>(&mut self, widget: W)
where
W: MakeWidget,
{
self.ordered.push(widget.make_widget());
}
/// Adds `widget` to self and returns the updated list.
pub fn with_widget<W>(mut self, widget: W) -> Self
where
W: MakeWidget,
{
self.push(widget);
self
}
/// Returns the number of widgets in this list.
#[must_use]
pub fn len(&self) -> usize {
self.ordered.len()
}
/// Returns true if there are no widgets in this list.
#[must_use]
pub fn is_empty(&self) -> bool {
self.ordered.is_empty()
}
}
impl<W> FromIterator<W> for Widgets
where
W: MakeWidget,
{
fn from_iter<T: IntoIterator<Item = W>>(iter: T) -> Self {
Self {
ordered: iter.into_iter().map(MakeWidget::make_widget).collect(),
}
}
}
impl Deref for Widgets {
type Target = [BoxedWidget];
fn deref(&self) -> &Self::Target {
&self.ordered
}
}

View file

@ -1,3 +1,5 @@
//! Built-in [`Widget`](crate::widget::Widget) implementations.
pub mod array;
mod button;
mod canvas;

View file

@ -1,27 +1,35 @@
//! A widget that combines an array of [`Widgets`] into one.
use std::ops::Deref;
use alot::{LotId, OrderedLots};
use kludgine::figures::units::UPx;
use kludgine::figures::{Point, Rect, Size};
use crate::children::Children;
use crate::context::{AsEventContext, EventContext, GraphicsContext};
use crate::widget::{IntoValue, ManagedWidget, Value, Widget};
use crate::value::{Generation, IntoValue, Value};
use crate::widget::{ManagedWidget, Widget, Widgets};
use crate::ConstraintLimit;
/// A widget that displays a collection of [`Widgets`] in a
/// [direction](ArrayDirection).
#[derive(Debug)]
pub struct Array {
/// The direction to display the children using.
pub direction: Value<ArrayDirection>,
pub children: Value<Children>,
/// The children widgets that belong to this array.
pub children: Value<Widgets>,
layout: Layout,
layout_generation: Option<usize>,
layout_generation: Option<Generation>,
// TODO Refactor synced_children into its own type.
synced_children: Vec<ManagedWidget>,
}
impl Array {
/// Returns a new widget with the given direction and widgets.
pub fn new(
direction: impl IntoValue<ArrayDirection>,
children: impl IntoValue<Children>,
widgets: impl IntoValue<Widgets>,
) -> Self {
let mut direction = direction.into_value();
@ -29,25 +37,27 @@ impl Array {
Self {
direction,
children: children.into_value(),
children: widgets.into_value(),
layout: Layout::new(initial_direction),
layout_generation: None,
synced_children: Vec::new(),
}
}
pub fn columns(children: impl IntoValue<Children>) -> Self {
Self::new(ArrayDirection::columns(), children)
/// Returns a new instance that displays `widgets` in a series of columns.
pub fn columns(widgets: impl IntoValue<Widgets>) -> Self {
Self::new(ArrayDirection::columns(), widgets)
}
pub fn rows(children: impl IntoValue<Children>) -> Self {
Self::new(ArrayDirection::rows(), children)
/// Returns a new instance that displays `widgets` in a series of rows.
pub fn rows(widgets: impl IntoValue<Widgets>) -> Self {
Self::new(ArrayDirection::rows(), widgets)
}
fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) {
let current_generation = self.children.generation();
if current_generation.map_or_else(
|| self.children.map(Children::len) != self.layout.children.len(),
|| self.children.map(Widgets::len) != self.layout.children.len(),
|gen| Some(gen) != self.layout_generation,
) {
self.layout_generation = self.children.generation();
@ -134,60 +144,99 @@ impl Widget for Array {
}
}
/// The direction of an [`Array`] widget.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct ArrayDirection {
/// The orientation of the widgets.
pub orientation: ArrayOrientation,
/// If true, the widgets will be laid out in reverse order.
pub reverse: bool,
}
/// The orientation (Row/Column) of an [`Array`] widget.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ArrayDirection {
Row { reverse: bool },
Column { reverse: bool },
pub enum ArrayOrientation {
/// The child widgets should be displayed as rows.
Row,
/// The child widgets should be displayed as columns.
Column,
}
impl ArrayDirection {
/// Display child widgets as columns.
#[must_use]
pub const fn columns() -> Self {
Self::Column { reverse: false }
Self {
orientation: ArrayOrientation::Column,
reverse: false,
}
}
/// Display child widgets as columns in reverse order.
#[must_use]
pub const fn columns_rev() -> Self {
Self::Column { reverse: true }
Self {
orientation: ArrayOrientation::Column,
reverse: true,
}
}
/// Display child widgets as rows.
#[must_use]
pub const fn rows() -> Self {
Self::Row { reverse: false }
Self {
orientation: ArrayOrientation::Row,
reverse: false,
}
}
/// Display child widgets as rows in reverse order.
#[must_use]
pub const fn rows_rev() -> Self {
Self::Row { reverse: true }
}
pub fn split_size<U>(&self, s: Size<U>) -> (U, U) {
match self {
Self::Row { .. } => (s.height, s.width),
Self::Column { .. } => (s.width, s.height),
Self {
orientation: ArrayOrientation::Row,
reverse: true,
}
}
pub fn make_size<U>(&self, measured: U, other: U) -> Size<U> {
match self {
Self::Row { .. } => Size::new(other, measured),
Self::Column { .. } => Size::new(measured, other),
/// Splits a size into its measured and other parts.
pub(crate) fn split_size<U>(self, s: Size<U>) -> (U, U) {
match self.orientation {
ArrayOrientation::Row => (s.height, s.width),
ArrayOrientation::Column => (s.width, s.height),
}
}
pub fn make_point<U>(&self, measured: U, other: U) -> Point<U> {
match self {
Self::Row { .. } => Point::new(other, measured),
Self::Column { .. } => Point::new(measured, other),
/// Combines split values into a [`Size`].
pub(crate) fn make_size<U>(self, measured: U, other: U) -> Size<U> {
match self.orientation {
ArrayOrientation::Row => Size::new(other, measured),
ArrayOrientation::Column => Size::new(measured, other),
}
}
/// Combines split values into a [`Point`].
pub(crate) fn make_point<U>(self, measured: U, other: U) -> Point<U> {
match self.orientation {
ArrayOrientation::Row => Point::new(other, measured),
ArrayOrientation::Column => Point::new(measured, other),
}
}
}
/// The strategy to use when laying a widget out inside of an [`Array`].
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ArrayDimension {
/// Attempt to lay out the widget based on its contents.
FitContent,
Fractional { weight: u8 },
/// Use a fractional amount of the available space.
Fractional {
/// The weight to apply to this widget when dividing multiple widgets
/// fractionally.
weight: u8,
},
/// Use an exact measurement for this widget's size.
Exact(UPx),
}

View file

@ -11,19 +11,23 @@ use kludgine::Color;
use crate::context::{EventContext, GraphicsContext};
use crate::names::Name;
use crate::styles::{
ComponentDefinition, ComponentGroup, ComponentName, HighlightColor, NamedComponent, TextColor,
};
use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED};
use crate::styles::components::{HighlightColor, TextColor};
use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent};
use crate::value::{IntoValue, Value};
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
/// A clickable button.
#[derive(Debug)]
pub struct Button {
/// The label to display on the button.
pub label: Value<String>,
/// The callback that is invoked when the button is clicked.
pub on_click: Option<Callback<()>>,
buttons_pressed: usize,
}
impl Button {
/// Returns a new button with the provided label.
pub fn new(label: impl IntoValue<String>) -> Self {
Self {
label: label.into_value(),
@ -32,6 +36,9 @@ impl Button {
}
}
/// Sets the `on_click` callback and returns self.
///
/// This callback will be invoked each time the button is clicked.
#[must_use]
pub fn on_click<F>(mut self, callback: F) -> Self
where
@ -76,7 +83,7 @@ impl Widget for Button {
.draw_shape(&background, Point::default(), None, None);
if context.focused() {
context.draw_focus_ring(&styles);
context.draw_focus_ring_using(&styles);
}
let width = context.graphics.size().width;
@ -198,7 +205,7 @@ impl Widget for Button {
}
HANDLED
} else {
UNHANDLED
IGNORED
}
}

View file

@ -1,21 +1,24 @@
use std::fmt::Debug;
use std::panic::UnwindSafe;
use std::time::{Duration, Instant};
use kludgine::figures::units::UPx;
use kludgine::figures::Size;
use crate::context::GraphicsContext;
use crate::value::Dynamic;
use crate::widget::Widget;
use crate::Tick;
/// A 2d drawable surface.
#[must_use]
pub struct Canvas {
render: Box<dyn RenderFunction>,
target_frame_duration: Option<Duration>,
last_frame_time: Option<Instant>,
tick: Option<Tick>,
redraw: Dynamic<()>,
}
impl Canvas {
/// Returns a new canvas that draws its contents by invoking `render`.
pub fn new<F>(render: F) -> Self
where
F: for<'clip, 'gfx, 'pass, 'context, 'window> FnMut(
@ -26,30 +29,24 @@ impl Canvas {
{
Self {
render: Box::new(render),
target_frame_duration: None,
last_frame_time: None,
tick: None,
redraw: Dynamic::new(()),
}
}
pub fn target_fps(mut self, fps: u16) -> Self {
const ONE_SECOND_NS: u64 = 1_000_000_000;
let frame_duration = ONE_SECOND_NS / u64::from(fps);
self.target_frame_duration = Some(Duration::from_nanos(frame_duration));
/// Associates a [`Tick`] with this widget and returns self.
pub fn tick(mut self, tick: Tick) -> Self {
self.tick = Some(tick);
self
}
}
impl Widget for Canvas {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
context.redraw_when_changed(&self.redraw);
self.render.render(context);
if let Some(target_frame_duration) = self.target_frame_duration {
let now = Instant::now();
let max_target = now + target_frame_duration;
let next_frame_target = self.last_frame_time.map_or(max_target, |last_frame_time| {
max_target.max(last_frame_time + target_frame_duration)
});
context.redraw_at(next_frame_target);
if let Some(tick) = &self.tick {
tick.rendered(context);
}
}

View file

@ -14,24 +14,30 @@ use kludgine::text::TextOrigin;
use kludgine::{Color, Kludgine};
use crate::context::{EventContext, WidgetContext};
use crate::styles::{HighlightColor, LineHeight, Styles, TextColor, TextSize};
use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize};
use crate::styles::Styles;
use crate::utils::ModifiersExt;
use crate::widget::{EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED};
use crate::value::{Generation, IntoValue, Value};
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
/// A text input widget.
#[must_use]
pub struct Input {
/// The value of this widget.
pub text: Value<String>,
editor: Option<LiveEditor>,
cursor_state: CursorState,
}
impl Input {
/// Returns an empty widget.
pub fn empty() -> Self {
Self::new(String::new())
}
/// Returns a new widget containing `initial_text`.
pub fn new(initial_text: impl IntoValue<String>) -> Self {
Self {
text: initial_text.into_value(),
@ -149,7 +155,7 @@ impl Widget for Input {
buffer.shape_until_scroll(context.graphics.font_system());
if context.focused() {
context.draw_focus_ring(&styles);
context.draw_focus_ring_using(&styles);
context.set_ime_allowed(true);
let line_height = Px::from_float(buffer.metrics().line_height);
if let Some(selection) = selection {
@ -310,7 +316,7 @@ impl Widget for Input {
context: &mut EventContext<'_, '_>,
) -> EventHandling {
if !input.state.is_pressed() {
return UNHANDLED;
return IGNORED;
}
let styles = context.query_style(&[&TextColor]);
@ -362,7 +368,7 @@ impl Widget for Input {
editor.insert_string(&text, None);
HANDLED
}
(_, _) => UNHANDLED,
(_, _) => IGNORED,
};
if handled.is_break() {
@ -400,7 +406,7 @@ impl Widget for Input {
struct LiveEditor {
editor: Editor,
generation: Option<usize>,
generation: Option<Generation>,
}
fn cursor_glyph(buffer: &Buffer, cursor: &Cursor) -> Result<(Point<Px>, Px), NotVisible> {

View file

@ -3,30 +3,34 @@ use kludgine::figures::{Point, Size};
use kludgine::text::{Text, TextOrigin};
use crate::context::GraphicsContext;
use crate::styles::TextColor;
use crate::widget::{IntoValue, Value, Widget};
use crate::styles::components::TextColor;
use crate::value::{IntoValue, Value};
use crate::widget::Widget;
/// A read-only text widget.
#[derive(Debug)]
pub struct Label {
pub contents: Value<String>,
/// The contents of the label.
pub text: Value<String>,
}
impl Label {
pub fn new(contents: impl IntoValue<String>) -> Self {
/// Returns a new label that displays `text`.
pub fn new(text: impl IntoValue<String>) -> Self {
Self {
contents: contents.into_value(),
text: text.into_value(),
}
}
}
impl Widget for Label {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
self.contents.redraw_when_changed(context);
self.text.redraw_when_changed(context);
let center = Point::from(context.graphics.size()) / 2;
let styles = context.query_style(&[&TextColor]);
let width = context.graphics.size().width;
self.contents.map(|contents| {
self.text.map(|contents| {
context.graphics.draw_text(
Text::new(contents, styles.get_or_default(&TextColor))
.origin(TextOrigin::Center)
@ -44,7 +48,7 @@ impl Widget for Label {
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
self.contents.map(|contents| {
self.text.map(|contents| {
context
.graphics
.measure_text(Text::from(contents).wrap_at(width))

View file

@ -6,6 +6,7 @@ use crate::styles::Styles;
use crate::widget::{BoxedWidget, ManagedWidget, Widget};
use crate::ConstraintLimit;
/// A widget that applies a set of [`Styles`] to all contained widgets.
#[derive(Debug)]
pub struct Style {
styles: Styles,
@ -14,6 +15,8 @@ pub struct Style {
}
impl Style {
/// Returns a new widget that applies `styles` to `child` and any children
/// it may have.
pub fn new(styles: impl Into<Styles>, child: impl Widget) -> Self {
Self {
styles: styles.into(),

View file

@ -1,26 +1,24 @@
use std::fmt::Debug;
use std::panic::UnwindSafe;
use kludgine::figures::utils::lossy_f64_to_f32;
use crate::context::{EventContext, GraphicsContext};
use crate::dynamic::Dynamic;
use crate::kludgine::app::winit::event::{DeviceId, KeyEvent, MouseScrollDelta, TouchPhase};
use crate::kludgine::app::winit::keyboard::Key;
use crate::kludgine::figures::units::UPx;
use crate::kludgine::figures::Size;
use crate::kludgine::tilemap;
use crate::kludgine::tilemap::TileMapFocus;
use crate::tick::Tick;
use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED};
use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
use crate::ConstraintLimit;
/// A layered tile-based 2d game surface.
#[derive(Debug)]
#[must_use]
pub struct TileMap<Layers> {
layers: Value<Layers>,
focus: Value<TileMapFocus>,
key: Option<Callback<Key, EventHandling>>,
zoom: f32,
tick: Option<Tick>,
}
@ -31,32 +29,30 @@ impl<Layers> TileMap<Layers> {
layers,
focus: Value::Constant(TileMapFocus::default()),
zoom: 1.,
key: None,
tick: None,
}
}
/// Returns a new tilemap that contains dynamic layers.
pub fn dynamic(layers: Dynamic<Layers>) -> Self {
Self::construct(Value::Dynamic(layers))
}
/// Returns a new tilemap that renders `layers`.
pub fn new(layers: Layers) -> Self {
Self::construct(Value::Constant(layers))
}
/// Sets the camera's focus and returns self.
///
/// The tilemap will ensure that `focus` is centered.
// TODO how do we allow the camera to "lag" for juice effects?
pub fn focus_on(mut self, focus: impl IntoValue<TileMapFocus>) -> Self {
self.focus = focus.into_value();
self
}
pub fn on_key<F>(mut self, key: F) -> Self
where
F: FnMut(Key) -> EventHandling + Send + UnwindSafe + 'static,
{
self.key = Some(Callback::new(key));
self
}
/// Associates a [`Tick`] with this widget and returns self.
pub fn tick(mut self, tick: Tick) -> Self {
self.tick = Some(tick);
self
@ -68,15 +64,15 @@ where
Layers: tilemap::Layers,
{
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
self.focus.redraw_when_changed(context);
self.layers.redraw_when_changed(context);
let focus = self.focus.get();
self.layers
.map(|layers| tilemap::draw(layers, focus, self.zoom, &mut context.graphics));
if let Some(tick) = &self.tick {
tick.rendered();
tick.rendered(context);
} else {
self.focus.redraw_when_changed(context);
self.layers.redraw_when_changed(context);
}
}
@ -116,13 +112,7 @@ where
if let Some(tick) = &self.tick {
tick.key_input(&input)?;
}
if !input.state.is_pressed() {
return UNHANDLED;
}
if let Some(on_key) = &mut self.key {
on_key.invoke(input.logical_key.clone())?;
}
UNHANDLED
IGNORED
}
}

View file

@ -1,9 +1,11 @@
//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop
//! window.
use std::cell::RefCell;
use std::collections::HashMap;
use std::panic::{AssertUnwindSafe, UnwindSafe};
use kludgine::app::winit::dpi::PhysicalPosition;
use kludgine::app::winit::error::EventLoopError;
use kludgine::app::winit::event::{
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
@ -18,19 +20,24 @@ use crate::context::{EventContext, Exclusive, GraphicsContext, WidgetContext};
use crate::graphics::Graphics;
use crate::tree::Tree;
use crate::utils::ModifiersExt;
use crate::widget::{BoxedWidget, EventHandling, ManagedWidget, Widget, HANDLED, UNHANDLED};
use crate::widget::{BoxedWidget, EventHandling, ManagedWidget, Widget, HANDLED, IGNORED};
use crate::window::sealed::WindowCommand;
use crate::Run;
/// A currently running Gooey window.
pub type RunningWindow<'window> = kludgine::app::Window<'window, WindowCommand>;
/// The attributes of a Gooey window.
pub type WindowAttributes = kludgine::app::WindowAttributes<WindowCommand>;
/// A Gooey window that is not yet running.
#[must_use]
pub struct Window<Behavior>
where
Behavior: WindowBehavior,
{
context: Behavior::Context,
/// The attributes of this window.
pub attributes: WindowAttributes,
}
@ -46,6 +53,7 @@ where
}
impl Window<BoxedWidget> {
/// Returns a new instance using `widget` as its contents.
pub fn for_widget<W>(widget: W) -> Self
where
W: Widget,
@ -58,6 +66,8 @@ impl<Behavior> Window<Behavior>
where
Behavior: WindowBehavior,
{
/// Returns a new instance using `context` to initialize the window upon
/// opening.
pub fn new(context: Behavior::Context) -> Self {
Self {
attributes: WindowAttributes {
@ -73,36 +83,45 @@ impl<Behavior> Run for Window<Behavior>
where
Behavior: WindowBehavior,
{
fn run(self) -> crate::Result<(), EventLoopError> {
fn run(self) -> crate::Result {
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe((
self.context,
RefCell::new(WindowSettings {
RefCell::new(sealed::WindowSettings {
attributes: Some(self.attributes),
}),
)))
}
}
/// The behavior of a Gooey window.
pub trait WindowBehavior: Sized + 'static {
/// The type that is provided when initializing this window.
type Context: UnwindSafe + Send + 'static;
/// Return a new instance of this behavior using `context`.
fn initialize(window: &mut RunningWindow<'_>, context: Self::Context) -> Self;
/// Create the window's root widget. This function is only invoked once.
fn make_root(&mut self) -> BoxedWidget;
/// The window has been requested to close. If this function returns true,
/// the window will be closed. Returning false prevents the window from
/// closing.
#[allow(unused_variables)]
fn close_requested(&self, window: &mut RunningWindow<'_>) -> bool {
true
}
fn run() -> Result<(), EventLoopError>
/// Runs this behavior as an application.
fn run() -> crate::Result
where
Self::Context: Default,
{
Self::run_with(<Self::Context>::default())
}
fn run_with(context: Self::Context) -> Result<(), EventLoopError> {
/// Runs this behavior as an application, initialized with `context`.
fn run_with(context: Self::Context) -> crate::Result {
Window::<Self>::new(context).run()
}
}
@ -130,7 +149,7 @@ impl<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
where
T: WindowBehavior,
{
type Context = AssertUnwindSafe<(T::Context, RefCell<WindowSettings>)>;
type Context = AssertUnwindSafe<(T::Context, RefCell<sealed::WindowSettings>)>;
fn initialize(
mut window: RunningWindow<'_>,
@ -415,16 +434,12 @@ fn recursively_handle_event(
) -> Option<ManagedWidget> {
match each_widget(context) {
HANDLED => Some(context.widget().clone()),
UNHANDLED => context.parent().and_then(|parent| {
IGNORED => context.parent().and_then(|parent| {
recursively_handle_event(&mut context.for_other(&parent), each_widget)
}),
}
}
pub struct WindowSettings {
attributes: Option<WindowAttributes>,
}
#[derive(Default)]
struct MouseState {
location: Option<Point<Px>>,
@ -433,6 +448,12 @@ struct MouseState {
}
pub(crate) mod sealed {
use crate::window::WindowAttributes;
pub struct WindowSettings {
pub attributes: Option<WindowAttributes>,
}
pub enum WindowCommand {
Redraw,
// RequestClose,