Initial commit

This commit is contained in:
Jonathan Johnson 2023-10-18 08:22:41 -07:00
commit fc707835f5
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
21 changed files with 4978 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

2259
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "gooey"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [
"app",
] }
alot = "0.3"
# appit = { git = "https://github.com/khonsulabs/appit" }
[patch."https://github.com/khonsulabs/kludgine"]
kludgine = { path = "../kludgine2" }
[patch."https://github.com/khonsulabs/figures"]
figures = { path = "../figures" }

11
examples/button.rs Normal file
View file

@ -0,0 +1,11 @@
use gooey::dynamic::Dynamic;
use gooey::widget::Widget;
use gooey::widgets::Button;
use gooey::EventLoopError;
fn main() -> Result<(), EventLoopError> {
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)))
.run()
}

35
examples/canvas.rs Normal file
View file

@ -0,0 +1,35 @@
use gooey::widget::Widget;
use gooey::widgets::Canvas;
use kludgine::figures::units::Px;
use kludgine::figures::{Angle, IntoSigned, Point, Rect, Size};
use kludgine::shapes::Shape;
use kludgine::Color;
fn main() -> gooey::Result<()> {
let mut angle = Angle::degrees(0);
Canvas::new(move |graphics, _window| {
angle += Angle::degrees(1);
let center = Point::from(graphics.size()).into_signed() / 2;
graphics.draw_text(
"Canvas exposes the full power of Kludgine",
Color::WHITE,
kludgine::text::TextOrigin::Center,
center - Point::new(Px(0), Px(100)),
None,
None,
None,
);
graphics.draw_shape(
&Shape::filled_rect(
Rect::new(Point::new(Px(-50), Px(-50)), Size::new(Px(100), Px(100))),
Color::RED,
),
center,
Some(angle),
None,
)
})
.target_fps(60)
.run()
}

28
examples/counter.rs Normal file
View file

@ -0,0 +1,28 @@
use std::string::ToString;
use gooey::children::Children;
use gooey::dynamic::Dynamic;
use gooey::widget::Widget;
use gooey::widgets::array::Array;
use gooey::widgets::{Button, Label};
use gooey::EventLoopError;
fn main() -> Result<(), EventLoopError> {
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);
}
}))),
)
.run()
}

6
rustfmt.toml Normal file
View file

@ -0,0 +1,6 @@
unstable_features = true
use_field_init_shorthand = true
imports_granularity = "Module"
group_imports = "StdExternalCrate"
format_code_in_doc_comments = true
reorder_impl_items = true

69
src/children.rs Normal file
View file

@ -0,0 +1,69 @@
use std::ops::{Index, IndexMut};
use alot::OrderedLots;
use crate::widget::{BoxedWidget, Widget};
#[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: Widget,
{
self.ordered.push(BoxedWidget::new(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()
}
}

313
src/context.rs Normal file
View file

@ -0,0 +1,313 @@
use std::ops::{Deref, DerefMut};
use kludgine::app::winit::event::{DeviceId, MouseButton};
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, Point, Rect, Size};
use crate::dynamic::Dynamic;
use crate::graphics::Graphics;
use crate::tree::ManagedWidget;
use crate::widget::{BoxedWidget, EventHandling};
use crate::window::RunningWindow;
use crate::ConstraintLimit;
pub struct Context<'context, 'window> {
current_node: &'context ManagedWidget,
window: &'context mut RunningWindow<'window>,
pending_state: PendingState<'context>,
}
impl<'context, 'window> Context<'context, 'window> {
pub fn new(
current_node: &'context ManagedWidget,
window: &'context mut RunningWindow<'window>,
) -> Self {
Self {
current_node,
window,
pending_state: PendingState::Owned(PendingWidgetState {
focus: current_node
.tree
.focused_widget()
.map(|id| current_node.tree.widget(id)),
active: current_node
.tree
.active_widget()
.map(|id| current_node.tree.widget(id)),
}),
}
}
pub fn for_other<'child>(
&'child mut self,
widget: &'child ManagedWidget,
) -> Context<'child, 'window> {
Context {
current_node: widget,
window: &mut *self.window,
pending_state: self.pending_state.borrowed(),
}
}
pub(crate) fn parent(&self) -> Option<ManagedWidget> {
self.current_node.parent()
}
pub fn redraw_when_changed<T>(&self, value: &Dynamic<T>) {
value.redraw_when_changed(self.window.handle());
}
pub fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>) {
// 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(graphics.clip_rect().into_signed());
self.current_node.lock().redraw(graphics, self);
}
pub fn measure(
&mut self,
available_space: Size<ConstraintLimit>,
graphics: &mut Graphics<'_, '_, '_>,
) -> Size<UPx> {
self.current_node
.lock()
.measure(available_space, graphics, self)
}
pub fn hit_test(&mut self, location: Point<Px>) -> bool {
self.current_node.lock().hit_test(location, self)
}
pub fn mouse_down(
&mut self,
location: Point<Px>,
device_id: DeviceId,
button: MouseButton,
) -> EventHandling {
self.current_node
.lock()
.mouse_down(location, device_id, button, self)
}
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);
}
pub fn mouse_up(
&mut self,
location: Option<Point<Px>>,
device_id: DeviceId,
button: MouseButton,
) {
self.current_node
.lock()
.mouse_up(location, device_id, button, self);
}
#[must_use]
pub fn push_child(&self, child: BoxedWidget) -> ManagedWidget {
self.current_node
.tree
.push_boxed(child, Some(self.current_node))
}
pub fn remove_child(&self, child: ManagedWidget) {
self.current_node
.tree
.remove_child(child, self.current_node);
}
#[must_use]
pub fn last_rendered_at(&self) -> Option<Rect<Px>> {
self.current_node.last_rendered_at()
}
pub(crate) fn hover(&mut self, location: Point<Px>) {
let newly_hovered = match self.current_node.tree.hover(Some(self.current_node)) {
Ok(old_hover) => {
if let Some(old_hover) = old_hover {
let mut old_hover_context = self.for_other(&old_hover);
old_hover.lock().unhover(&mut old_hover_context);
}
true
}
Err(_) => false,
};
if newly_hovered {
self.current_node.lock().hover(location, self);
}
}
pub(crate) fn clear_hover(&mut self) {
if let Ok(Some(old_hover)) = self.current_node.tree.hover(None) {
let mut old_hover_context = self.for_other(&old_hover);
old_hover.lock().unhover(&mut old_hover_context);
}
}
pub fn focus(&mut self) {
self.pending_state.focus = Some(self.current_node.clone());
}
pub(crate) fn clear_focus(&mut self) {
self.pending_state.focus = None;
}
pub fn blur(&mut self) -> bool {
if self.focused() {
self.clear_focus();
true
} else {
false
}
}
pub fn activate(&mut self) -> bool {
if self
.pending_state
.active
.as_ref()
.map_or(true, |active| active != self.current_node)
{
self.pending_state.active = Some(self.current_node.clone());
true
} else {
false
}
}
pub fn deactivate(&mut self) -> bool {
if self.active() {
self.clear_active();
true
} else {
false
}
}
pub(crate) fn clear_active(&mut self) {
self.pending_state.active = None;
}
#[must_use]
pub fn active(&self) -> bool {
self.pending_state.active.as_ref() == Some(self.current_node)
}
#[must_use]
pub fn hovered(&self) -> bool {
self.current_node.hovered()
}
#[must_use]
pub fn focused(&self) -> bool {
self.pending_state.focus.as_ref() == Some(self.current_node)
}
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);
}
}
}
}
#[must_use]
pub const fn widget(&self) -> &ManagedWidget {
self.current_node
}
}
impl Drop for Context<'_, '_> {
fn drop(&mut self) {
if matches!(self.pending_state, PendingState::Owned(_)) {
self.apply_pending_state();
}
}
}
impl<'window> Deref for Context<'_, 'window> {
type Target = RunningWindow<'window>;
fn deref(&self) -> &Self::Target {
self.window
}
}
impl<'window> DerefMut for Context<'_, 'window> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.window
}
}
enum PendingState<'a> {
Borrowed(&'a mut PendingWidgetState),
Owned(PendingWidgetState),
}
#[derive(Default)]
struct PendingWidgetState {
focus: Option<ManagedWidget>,
active: Option<ManagedWidget>,
}
impl PendingState<'_> {
pub fn borrowed(&mut self) -> PendingState<'_> {
PendingState::Borrowed(self)
}
}
impl Deref for PendingState<'_> {
type Target = PendingWidgetState;
fn deref(&self) -> &Self::Target {
match self {
PendingState::Borrowed(state) => state,
PendingState::Owned(state) => state,
}
}
}
impl DerefMut for PendingState<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
PendingState::Borrowed(state) => state,
PendingState::Owned(state) => state,
}
}
}

307
src/dynamic.rs Normal file
View file

@ -0,0 +1,307 @@
use std::fmt::Debug;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use kludgine::app::WindowHandle;
use crate::window::sealed::WindowCommand;
#[derive(Debug)]
pub struct Dynamic<T>(Arc<DynamicData<T>>);
impl<T> Dynamic<T> {
pub fn new(value: T) -> Self {
Self(Arc::new(DynamicData {
state: Mutex::new(State {
wrapped: GenerationalValue {
value,
generation: 0,
},
callbacks: Vec::new(),
windows: Vec::new(),
readers: 0,
}),
sync: Condvar::new(),
}))
}
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
let state = self.state();
map(&state.wrapped.value)
}
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
let mut state = self.state();
state.wrapped.generation = state.wrapped.generation.wrapping_add(1);
let result = map(&mut state.wrapped.value);
drop(state);
self.0.sync.notify_all();
result
}
pub fn for_each<F>(&self, mut map: F)
where
F: for<'a> FnMut(&'a T) + Send + 'static,
{
self.0.for_each(move |gen| map(&gen.value));
}
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
R: Send + 'static,
{
self.0.map_each(move |gen| map(&gen.value))
}
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>) {
self.0.redraw_when_changed(window);
}
#[must_use]
pub fn get(&self) -> T
where
T: Clone,
{
self.0.get().value
}
#[must_use]
pub fn replace(&self, new_value: T) -> T {
self.0.replace(new_value).value
}
pub fn set(&self, new_value: T) {
let _old = self.replace(new_value);
}
#[must_use]
pub fn create_ref_reader(&self) -> DynamicRefReader<T> {
self.state().readers += 1;
DynamicRefReader {
source: self.0.clone(),
read_generation: self.0.state().wrapped.generation,
}
}
fn state(&self) -> MutexGuard<'_, State<T>> {
self.0.state()
}
#[must_use]
pub fn generation(&self) -> usize {
self.state().wrapped.generation
}
}
impl<T> Clone for Dynamic<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Drop for Dynamic<T> {
fn drop(&mut self) {
let state = self.state();
if state.readers == 0 {
drop(state);
self.0.sync.notify_all();
}
}
}
impl<T> From<Dynamic<T>> for DynamicRefReader<T> {
fn from(value: Dynamic<T>) -> Self {
value.create_ref_reader()
}
}
#[derive(Debug)]
struct DynamicData<T> {
state: Mutex<State<T>>,
sync: Condvar,
}
impl<T> DynamicData<T> {
fn state(&self) -> MutexGuard<'_, State<T>> {
self.state
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
}
pub fn redraw_when_changed(&self, window: WindowHandle<WindowCommand>) {
let mut state = self.state();
state.windows.push(window);
}
#[must_use]
pub fn get(&self) -> GenerationalValue<T>
where
T: Clone,
{
self.state().wrapped.clone()
}
#[must_use]
pub fn replace(&self, new_value: T) -> GenerationalValue<T> {
let mut state = self.state();
let old = {
let state = &mut *state;
let generation = state.wrapped.generation.wrapping_add(1);
let old = std::mem::replace(
&mut state.wrapped,
GenerationalValue {
value: new_value,
generation,
},
);
for callback in &mut state.callbacks {
callback.update(&state.wrapped);
}
for window in state.windows.drain(..) {
let _result = window.send(WindowCommand::Redraw);
}
old
};
drop(state);
self.sync.notify_all();
old
}
pub fn for_each<F>(&self, map: F)
where
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
{
let mut state = self.state();
state.callbacks.push(Box::new(map));
}
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
R: Send + 'static,
{
let mut state = self.state();
let initial_value = map(&state.wrapped);
let mapped_value = Dynamic::new(initial_value);
let returned = mapped_value.clone();
state
.callbacks
.push(Box::new(move |updated: &GenerationalValue<T>| {
mapped_value.set(map(updated));
}));
returned
}
}
struct State<T> {
wrapped: GenerationalValue<T>,
callbacks: Vec<Box<dyn ValueCallback<T>>>,
windows: Vec<WindowHandle<WindowCommand>>,
readers: usize,
}
impl<T> Debug for State<T>
where
T: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("State")
.field("wrapped", &self.wrapped)
.field("readers", &self.readers)
.finish_non_exhaustive()
}
}
trait ValueCallback<T>: Send {
fn update(&mut self, value: &GenerationalValue<T>);
}
impl<T, F> ValueCallback<T> for F
where
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
{
fn update(&mut self, value: &GenerationalValue<T>) {
self(value);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GenerationalValue<T> {
pub value: T,
pub generation: usize,
}
#[derive(Debug)]
pub struct DynamicRefReader<T> {
source: Arc<DynamicData<T>>,
read_generation: usize,
}
impl<T> DynamicRefReader<T> {
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)
}
#[must_use]
pub fn get(&self) -> T
where
T: Clone,
{
self.source.get().value
}
pub fn block_until_updated(&mut self) -> bool {
let mut state = self.source.state();
loop {
if state.wrapped.generation != self.read_generation {
return true;
} else if state.readers == Arc::strong_count(&self.source) {
return false;
}
state = self
.source
.sync
.wait(state)
.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> {
fn clone(&self) -> Self {
self.source.state().readers += 1;
Self {
source: self.source.clone(),
read_generation: self.read_generation,
}
}
}
impl<T> Drop for DynamicRefReader<T> {
fn drop(&mut self) {
let mut state = self.source.state();
state.readers -= 1;
}
}
#[test]
fn disconnecting_reader_from_dynamic() {
let value = Dynamic::new(1);
let mut ref_reader = value.create_ref_reader();
drop(value);
assert!(!ref_reader.block_until_updated());
}

48
src/graphics.rs Normal file
View file

@ -0,0 +1,48 @@
use std::ops::{Deref, DerefMut};
use kludgine::figures::units::UPx;
use kludgine::figures::Rect;
pub struct Graphics<'clip, 'gfx, 'pass> {
renderer: GraphicsContext<'clip, 'gfx, 'pass>,
}
enum GraphicsContext<'clip, 'gfx, 'pass> {
Renderer(kludgine::render::Renderer<'gfx, 'pass>),
Clipped(kludgine::ClipGuard<'clip, kludgine::render::Renderer<'gfx, 'pass>>),
}
impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
#[must_use]
pub fn new(renderer: kludgine::render::Renderer<'gfx, 'pass>) -> Self {
Self {
renderer: GraphicsContext::Renderer(renderer),
}
}
pub fn clipped_to(&mut self, clip: Rect<UPx>) -> Graphics<'_, 'gfx, 'pass> {
Graphics {
renderer: GraphicsContext::Clipped(self.deref_mut().clipped_to(clip)),
}
}
}
impl<'gfx, 'pass> Deref for Graphics<'_, 'gfx, 'pass> {
type Target = kludgine::render::Renderer<'gfx, 'pass>;
fn deref(&self) -> &Self::Target {
match &self.renderer {
GraphicsContext::Renderer(renderer) => renderer,
GraphicsContext::Clipped(clipped) => clipped,
}
}
}
impl<'gfx, 'pass> DerefMut for Graphics<'_, 'gfx, 'pass> {
fn deref_mut(&mut self) -> &mut Self::Target {
match &mut self.renderer {
GraphicsContext::Renderer(renderer) => renderer,
GraphicsContext::Clipped(clipped) => &mut *clipped,
}
}
}

37
src/lib.rs Normal file
View file

@ -0,0 +1,37 @@
#![warn(clippy::pedantic)]
#![allow(
clippy::module_name_repetitions,
clippy::missing_errors_doc,
clippy::missing_panics_doc
)]
pub mod children;
pub mod context;
pub mod dynamic;
pub mod graphics;
mod tree;
mod utils;
pub mod widget;
pub mod widgets;
pub mod window;
pub use kludgine::app::winit::error::EventLoopError;
pub use kludgine::app::winit::event::ElementState;
use kludgine::figures::units::UPx;
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum ConstraintLimit {
Known(UPx),
ClippedAfter(UPx),
}
impl ConstraintLimit {
#[must_use]
pub fn max(self) -> UPx {
match self {
ConstraintLimit::Known(v) | ConstraintLimit::ClippedAfter(v) => v,
}
}
}
pub type Result<T, E = EventLoopError> = std::result::Result<T, E>;

251
src/tree.rs Normal file
View file

@ -0,0 +1,251 @@
use std::fmt::Debug;
use std::mem;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use alot::{LotId, Lots};
use kludgine::figures::units::Px;
use kludgine::figures::{Point, Rect};
use crate::widget::{BoxedWidget, Widget};
#[derive(Clone, Default)]
pub struct Tree {
data: Arc<Mutex<TreeData>>,
}
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 {
widget: widget.clone(),
children: Vec::new(),
parent: parent.map(|parent| parent.id),
last_rendered_location: None,
}));
if let Some(parent) = parent {
let parent = &mut data.nodes[parent.id.0];
parent.children.push(id);
}
ManagedWidget {
id,
widget,
tree: self.clone(),
}
}
#[allow(clippy::needless_pass_by_value)] // This is sort of a destructor type call
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>) {
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>> {
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.nodes[widget.0].last_rendered_location
}
pub(crate) fn reset_render_order(&self) {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.render_order.clear();
}
pub fn hover(&self, new_hover: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.update_tracked_widget(new_hover, self, |data| &mut data.hover)
}
pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.update_tracked_widget(new_focus, self, |data| &mut data.focus)
}
pub fn activate(
&self,
new_active: Option<&ManagedWidget>,
) -> Result<Option<ManagedWidget>, ()> {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.update_tracked_widget(new_active, self, |data| &mut data.active)
}
pub fn widget(&self, id: WidgetId) -> ManagedWidget {
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
ManagedWidget {
id,
widget: data.nodes[id.0].widget.clone(),
tree: self.clone(),
}
}
pub fn active_widget(&self) -> Option<WidgetId> {
self.data
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.active
}
pub fn hovered_widget(&self) -> Option<WidgetId> {
self.data
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.hover
}
pub fn focused_widget(&self) -> Option<WidgetId> {
self.data
.lock()
.map_or_else(PoisonError::into_inner, |g| g)
.focus
}
pub(crate) fn widgets_at_point(&self, point: Point<Px>) -> Vec<ManagedWidget> {
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
let mut hits = Vec::new();
for id in data.render_order.iter().rev() {
if let Some(last_rendered) = data.nodes[id.0].last_rendered_location {
if last_rendered.contains(point) {
hits.push(ManagedWidget {
id: *id,
widget: data.nodes[id.0].widget.clone(),
tree: self.clone(),
});
}
}
}
hits
}
pub(crate) fn parent(&self, id: WidgetId) -> Option<WidgetId> {
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
data.nodes[id.0].parent
}
}
#[derive(Default)]
struct TreeData {
nodes: Lots<Node>,
active: Option<WidgetId>,
focus: Option<WidgetId>,
hover: Option<WidgetId>,
render_order: Vec<WidgetId>,
}
impl TreeData {
fn remove_child(&mut self, child: WidgetId, parent: WidgetId) {
let removed_node = self.nodes.remove(child.0).expect("widget already removed");
let parent = &mut self.nodes[parent.0];
let index = parent
.children
.iter()
.enumerate()
.find_map(|(index, c)| (*c == child).then_some(index))
.expect("child not found in parent");
parent.children.remove(index);
let mut detached_nodes = removed_node.children;
while let Some(node) = detached_nodes.pop() {
let mut node = self.nodes.remove(node.0).expect("detached node missing");
detached_nodes.append(&mut node.children);
}
}
fn update_tracked_widget(
&mut self,
new_widget: Option<&ManagedWidget>,
tree: &Tree,
property: impl FnOnce(&mut Self) -> &mut Option<WidgetId>,
) -> Result<Option<ManagedWidget>, ()> {
match (
mem::replace(property(self), new_widget.map(|w| w.id)),
new_widget,
) {
(Some(old_widget), Some(new_widget)) if old_widget == new_widget.id => Err(()),
(Some(old_widget), _) => Ok(Some(ManagedWidget {
id: old_widget,
widget: self.nodes[old_widget.0].widget.clone(),
tree: tree.clone(),
})),
(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
}
}
pub struct Node {
pub widget: BoxedWidget,
pub children: Vec<WidgetId>,
pub parent: Option<WidgetId>,
pub last_rendered_location: Option<Rect<Px>>,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct WidgetId(LotId);

17
src/utils.rs Normal file
View file

@ -0,0 +1,17 @@
use kludgine::app::winit::keyboard::ModifiersState;
pub trait ModifiersExt {
fn primary(&self) -> bool;
}
impl ModifiersExt for ModifiersState {
#[cfg(any(target_os = "macos", target_os = "ios"))]
fn primary(&self) -> bool {
self.super_key()
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
fn primary(&self) -> bool {
self.control_key()
}
}

235
src/widget.rs Normal file
View file

@ -0,0 +1,235 @@
use std::clone::Clone;
use std::fmt::Debug;
use std::ops::ControlFlow;
use std::panic::UnwindSafe;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use kludgine::app::winit::error::EventLoopError;
use kludgine::app::winit::event::{DeviceId, MouseButton};
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{Point, Size};
use crate::context::Context;
use crate::dynamic::Dynamic;
use crate::graphics::Graphics;
use crate::window::{RunningWindow, Window, WindowBehavior};
use crate::ConstraintLimit;
pub trait Widget: Send + UnwindSafe + Debug + 'static {
fn run(self) -> Result<(), EventLoopError>
where
Self: Sized,
{
Window::<WidgetWindow<Self>>::new(WidgetWindow(Some(self))).run()
}
fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context<'_, '_>);
fn measure(
&mut self,
available_space: Size<ConstraintLimit>,
graphics: &mut Graphics<'_, '_, '_>,
context: &mut Context<'_, '_>,
) -> Size<UPx>;
#[allow(unused_variables)]
fn hit_test(&mut self, location: Point<Px>, context: &mut Context<'_, '_>) -> bool {
false
}
#[allow(unused_variables)]
fn hover(&mut self, location: Point<Px>, context: &mut Context<'_, '_>) {}
#[allow(unused_variables)]
fn unhover(&mut self, context: &mut Context<'_, '_>) {}
#[allow(unused_variables)]
fn focus(&mut self, context: &mut Context<'_, '_>) {}
#[allow(unused_variables)]
fn blur(&mut self, context: &mut Context<'_, '_>) {}
#[allow(unused_variables)]
fn activate(&mut self, context: &mut Context<'_, '_>) {}
#[allow(unused_variables)]
fn deactivate(&mut self, context: &mut Context<'_, '_>) {}
#[allow(unused_variables)]
fn mouse_down(
&mut self,
location: Point<Px>,
device_id: DeviceId,
button: MouseButton,
context: &mut Context<'_, '_>,
) -> EventHandling {
UNHANDLED
}
#[allow(unused_variables)]
fn mouse_drag(
&mut self,
location: Point<Px>,
device_id: DeviceId,
button: MouseButton,
context: &mut Context<'_, '_>,
) {
}
#[allow(unused_variables)]
fn mouse_up(
&mut self,
location: Option<Point<Px>>,
device_id: DeviceId,
button: MouseButton,
context: &mut Context<'_, '_>,
) {
}
}
pub type EventHandling = ControlFlow<EventHandled, EventIgnored>;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct EventHandled;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
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>>);
impl BoxedWidget {
pub fn new<W>(widget: W) -> Self
where
W: Widget,
{
Self(Arc::new(Mutex::new(widget)))
}
pub(crate) fn lock(&self) -> MutexGuard<'_, dyn Widget> {
self.0.lock().map_or_else(PoisonError::into_inner, |g| g)
}
}
impl Eq for BoxedWidget {}
impl PartialEq for BoxedWidget {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Debug)]
pub enum Value<T>
where
T: 'static,
{
Static(T),
Dynamic(Dynamic<T>),
}
impl<T> Value<T> {
pub fn map<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
match self {
Value::Static(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::Static(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::Static(_) => None,
Value::Dynamic(value) => Some(value.generation()),
}
}
}
pub trait IntoValue<T> {
fn into_value(self) -> Value<T>;
}
impl<T> IntoValue<T> for T {
fn into_value(self) -> Value<T> {
Value::Static(self)
}
}
impl<'a> IntoValue<String> for &'a str {
fn into_value(self) -> Value<String> {
Value::Static(self.to_owned())
}
}
impl<T> IntoValue<T> for Dynamic<T> {
fn into_value(self) -> Value<T> {
Value::Dynamic(self)
}
}
pub struct Callback<T>(Box<dyn CallbackFunction<T>>);
impl<T> Debug for Callback<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Callback")
.field(&(self as *const Self))
.finish()
}
}
impl<T> Callback<T> {
pub fn new<F>(function: F) -> Self
where
F: FnMut(T) + Send + UnwindSafe + 'static,
{
Self(Box::new(function))
}
pub fn invoke(&mut self, value: T) {
self.0.invoke(value);
}
}
trait CallbackFunction<T>: Send + UnwindSafe {
fn invoke(&mut self, value: T);
}
impl<T, F> CallbackFunction<T> for F
where
F: FnMut(T) + Send + UnwindSafe,
{
fn invoke(&mut self, value: T) {
self(value);
}
}

8
src/widgets.rs Normal file
View file

@ -0,0 +1,8 @@
pub mod array;
mod button;
mod canvas;
mod label;
pub use button::Button;
pub use canvas::Canvas;
pub use label::Label;

575
src/widgets/array.rs Normal file
View file

@ -0,0 +1,575 @@
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::Context;
use crate::graphics::Graphics;
use crate::tree::ManagedWidget;
use crate::widget::{IntoValue, Value, Widget};
use crate::ConstraintLimit;
#[derive(Debug)]
pub struct Array {
pub direction: Value<ArrayDirection>,
pub children: Value<Children>,
layout: Layout,
layout_generation: Option<usize>,
synced_children: Vec<ManagedWidget>,
}
impl Array {
pub fn new(
direction: impl IntoValue<ArrayDirection>,
children: impl IntoValue<Children>,
) -> Self {
let mut direction = direction.into_value();
let initial_direction = direction.get();
Self {
direction,
children: children.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)
}
pub fn rows(children: impl IntoValue<Children>) -> Self {
Self::new(ArrayDirection::rows(), children)
}
fn synchronize_children(&mut self, context: &mut Context<'_, '_>) {
let current_generation = self.children.generation();
if current_generation.map_or_else(
|| self.children.map(Children::len) != self.layout.children.len(),
|gen| Some(gen) != self.layout_generation,
) {
self.layout_generation = self.children.generation();
self.children.map(|children| {
for (index, widget) in children.iter().enumerate() {
if self
.synced_children
.get(index)
.map_or(true, |child| child != widget)
{
// These entries do not match. See if we can find the
// new id somewhere else, if so we can swap the entries.
if let Some((swap_index, _)) = self
.synced_children
.iter()
.enumerate()
.skip(index + 1)
.find(|(_, child)| *child == widget)
{
self.synced_children.swap(index, swap_index);
self.layout.swap(index, swap_index);
} else {
// This is a brand new child.
self.synced_children
.insert(index, context.push_child(widget.clone()));
self.layout.insert(index, ArrayDimension::FitContent);
}
}
}
// 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);
}
self.layout.truncate(children.len());
});
}
}
}
impl Widget for Array {
fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context) {
self.synchronize_children(context);
self.layout.update(
Size::new(
ConstraintLimit::Known(graphics.size().width),
ConstraintLimit::Known(graphics.size().height),
),
|child_index, constraints| {
context
.for_other(&self.synced_children[child_index])
.measure(constraints, graphics)
},
);
for (index, layout) in self.layout.iter().enumerate() {
let child = &self.synced_children[index];
if layout.size > 0 {
let mut clipped = graphics.clipped_to(Rect::new(
self.layout.orientation.make_point(layout.offset, UPx(0)),
self.layout
.orientation
.make_size(layout.size, self.layout.other),
));
context.for_other(child).redraw(&mut clipped);
}
}
}
fn measure(
&mut self,
available_space: Size<ConstraintLimit>,
graphics: &mut Graphics<'_, '_, '_>,
context: &mut Context<'_, '_>,
) -> Size<UPx> {
self.synchronize_children(context);
self.layout
.update(available_space, |child_index, constraints| {
context
.for_other(&self.synced_children[child_index])
.measure(constraints, graphics)
})
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ArrayDirection {
Row { reverse: bool },
Column { reverse: bool },
}
impl ArrayDirection {
#[must_use]
pub const fn columns() -> Self {
Self::Column { reverse: false }
}
#[must_use]
pub const fn columns_rev() -> Self {
Self::Column { reverse: true }
}
#[must_use]
pub const fn rows() -> Self {
Self::Row { reverse: false }
}
#[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),
}
}
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),
}
}
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),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ArrayDimension {
FitContent,
Fractional { weight: u8 },
Exact(UPx),
}
#[derive(Debug)]
struct Layout {
children: OrderedLots<ArrayDimension>,
layouts: Vec<ArrayLayout>,
pub other: UPx,
total_weights: u32,
allocated_space: UPx,
fractional: Vec<(LotId, u8)>,
measured: Vec<LotId>,
pub orientation: ArrayDirection,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct ArrayLayout {
pub offset: UPx,
pub size: UPx,
}
impl Layout {
pub const fn new(orientation: ArrayDirection) -> Self {
Self {
orientation,
children: OrderedLots::new(),
layouts: Vec::new(),
other: UPx(0),
total_weights: 0,
allocated_space: UPx(0),
fractional: Vec::new(),
measured: Vec::new(),
}
}
#[cfg(test)] // only used in testing
pub fn push(&mut self, child: ArrayDimension) {
self.insert(self.len(), child);
}
pub fn remove(&mut self, index: usize) -> ArrayDimension {
let (id, dimension) = self.children.remove_by_index(index).expect("invalid index");
self.layouts.remove(index);
match dimension {
ArrayDimension::FitContent => {
self.measured.retain(|&measured| measured != id);
}
ArrayDimension::Fractional { weight } => {
self.fractional.retain(|(measured, _)| *measured != id);
self.total_weights -= u32::from(weight);
}
ArrayDimension::Exact(size) => {
self.allocated_space -= size;
}
}
dimension
}
pub fn truncate(&mut self, new_length: usize) {
while self.len() > new_length {
self.remove(self.len() - 1);
}
}
pub fn swap(&mut self, a: usize, b: usize) {
self.children.swap(a, b);
}
pub fn insert(&mut self, index: usize, child: ArrayDimension) {
let id = self.children.insert(index, child);
let layout = match child {
ArrayDimension::FitContent => {
self.measured.push(id);
UPx(0)
}
ArrayDimension::Fractional { weight } => {
self.total_weights += u32::from(weight);
self.fractional.push((id, weight));
UPx(0)
}
ArrayDimension::Exact(size) => {
self.allocated_space += size;
size
}
};
self.layouts.insert(
index,
ArrayLayout {
offset: UPx(0),
size: layout,
},
);
}
pub fn update(
&mut self,
available: Size<ConstraintLimit>,
mut measure: impl FnMut(usize, Size<ConstraintLimit>) -> Size<UPx>,
) -> Size<UPx> {
let (space_constraint, other_constraint) = self.orientation.split_size(available);
let available_space = space_constraint.max();
let mut remaining = available_space.saturating_sub(self.allocated_space);
// Measure the children that fit their content
for &id in &self.measured {
let index = self.children.index_of_id(id).expect("child not found");
if remaining > 0 {
let (measured, _) = self.orientation.split_size(measure(
index,
self.orientation
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
));
self.layouts[index].size = measured;
remaining = remaining.saturating_sub(measured);
} else {
self.layouts[index].size = UPx(0);
}
}
// Measure the weighted children within the remaining space
if self.total_weights > 0 {
let space_per_weight = remaining / self.total_weights;
remaining %= self.total_weights;
for (fractional_index, &(id, weight)) in self.fractional.iter().enumerate() {
let index = self.children.index_of_id(id).expect("child not found");
let size = space_per_weight * u32::from(weight);
self.layouts[index].size = size;
// If we have fractional amounts remaining, divide the pixels
if remaining > 0 {
let from_end = u32::try_from(self.fractional.len() - fractional_index)
.expect("too many items");
if remaining >= from_end {
let amount = (remaining + from_end - 1) / from_end;
remaining -= amount;
self.layouts[index].size += amount;
}
}
}
}
// Now that we know the constrained sizes, we can measure the children
// to get the other measurement using the constrainted measurement.
self.other = UPx(0);
let mut offset = UPx(0);
for index in 0..self.children.len() {
self.layouts[index].offset = offset;
offset += self.layouts[index].size;
let (_, measured) = self.orientation.split_size(measure(
index,
self.orientation.make_size(
ConstraintLimit::Known(self.layouts[index].size),
other_constraint,
),
));
self.other = self.other.max(measured);
}
self.other = match other_constraint {
ConstraintLimit::Known(max) => self.other.max(max),
ConstraintLimit::ClippedAfter(clip_limit) => self.other.min(clip_limit),
};
self.orientation.make_size(available_space, self.other)
}
}
impl Deref for Layout {
type Target = [ArrayLayout];
fn deref(&self) -> &Self::Target {
&self.layouts
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use kludgine::figures::units::UPx;
use kludgine::figures::Size;
use super::{ArrayDimension, ArrayDirection, Layout};
use crate::ConstraintLimit;
struct Child {
size: UPx,
dimension: ArrayDimension,
other: UPx,
divisible_by: Option<UPx>,
}
impl Child {
pub fn new(size: impl Into<UPx>, other: impl Into<UPx>) -> Self {
Self {
size: size.into(),
dimension: ArrayDimension::FitContent,
other: other.into(),
divisible_by: None,
}
}
pub fn fixed_size(mut self, size: UPx) -> Self {
self.dimension = ArrayDimension::Exact(size);
self
}
pub fn weighted(mut self, weight: u8) -> Self {
self.dimension = ArrayDimension::Fractional { weight };
self
}
pub fn divisible_by(mut self, split_at: impl Into<UPx>) -> Self {
self.divisible_by = Some(split_at.into());
self
}
}
fn assert_measured_children_in_orientation(
orientation: ArrayDirection,
children: &[Child],
available: Size<ConstraintLimit>,
expected: &[UPx],
expected_size: Size<UPx>,
) {
assert_eq!(children.len(), expected.len());
let mut flex = Layout::new(orientation);
for child in children {
flex.push(child.dimension);
}
let computed_size = flex.update(available, |index, constraints| {
let (measured_constraint, _other_constraint) = orientation.split_size(constraints);
let child = &children[index];
let maximum_measured = measured_constraint.max();
let (measured, other) = match (child.size.cmp(&maximum_measured), child.divisible_by) {
(Ordering::Greater, Some(divisible_by)) => {
let available_divided = maximum_measured / divisible_by;
let rows = ((child.size + divisible_by - 1) / divisible_by + available_divided
- 1)
/ available_divided;
(available_divided * divisible_by, child.other * rows)
}
_ => (child.size, child.other),
};
orientation.make_size(measured, other)
});
assert_eq!(computed_size, expected_size);
let mut offset = UPx(0);
for ((index, &child), &expected) in flex.iter().enumerate().zip(expected) {
assert_eq!(
child.size,
expected,
"child {index} measured to {}, expected {}",
child.size,
expected // TODO Display for UPx
);
assert_eq!(child.offset, offset);
offset += child.size;
}
}
fn assert_measured_children(
children: &[Child],
main_constraint: ConstraintLimit,
other_constraint: ConstraintLimit,
expected: &[UPx],
expected_measured: UPx,
expected_other: UPx,
) {
assert_measured_children_in_orientation(
ArrayDirection::rows(),
children,
ArrayDirection::rows().make_size(main_constraint, other_constraint),
expected,
ArrayDirection::rows().make_size(expected_measured, expected_other),
);
assert_measured_children_in_orientation(
ArrayDirection::columns(),
children,
ArrayDirection::columns().make_size(main_constraint, other_constraint),
expected,
ArrayDirection::columns().make_size(expected_measured, expected_other),
);
}
#[test]
fn size_to_fit() {
assert_measured_children(
&[Child::new(3, 1), Child::new(3, 1), Child::new(3, 1)],
ConstraintLimit::ClippedAfter(UPx(10)),
ConstraintLimit::ClippedAfter(UPx(10)),
&[UPx(3), UPx(3), UPx(3)],
UPx(10),
UPx(1),
);
}
#[test]
fn wrapping() {
// This tests some fun rounding edge cases. Because the total weights is
// 4 and the size is 10, we have inexact math to determine the pixel
// width of each child.
//
// In this particular example, it shows the weights are clamped so that
// each is credited for 2px. This is why the first child ends up with
// 4px. However, with 4 total weight, that leaves a remaining 2px to be
// assigned. The flex algorithm divides the remaining pixels amongst the
// remaining children.
assert_measured_children(
&[
Child::new(20, 1).divisible_by(3).weighted(2),
Child::new(3, 1).weighted(1),
Child::new(3, 1).weighted(1),
],
ConstraintLimit::Known(UPx(10)),
ConstraintLimit::ClippedAfter(UPx(10)),
&[UPx(4), UPx(3), UPx(3)],
UPx(10),
UPx(7), // 20 / 3 = 6.666, rounded up is 7
);
// Same as above, but with an 11px box. This creates a leftover of 3 px
// (11 % 4), adding 1px to all three children.
assert_measured_children(
&[
Child::new(20, 1).divisible_by(3).weighted(2),
Child::new(3, 1).weighted(1),
Child::new(3, 1).weighted(1),
],
ConstraintLimit::Known(UPx(11)),
ConstraintLimit::ClippedAfter(UPx(11)),
&[UPx(5), UPx(3), UPx(3)],
UPx(11),
UPx(7), // 20 / 3 = 6.666, rounded up is 7
);
// 12px box. This creates no leftover.
assert_measured_children(
&[
Child::new(20, 1).divisible_by(3).weighted(2),
Child::new(3, 1).weighted(1),
Child::new(3, 1).weighted(1),
],
ConstraintLimit::Known(UPx(12)),
ConstraintLimit::ClippedAfter(UPx(12)),
&[UPx(6), UPx(3), UPx(3)],
UPx(12),
UPx(4), // 20 / 6 = 3.666, rounded up is 4
);
// 13px box. This creates a leftover of 1 px (13 % 4), adding 1px only
// to the final child
assert_measured_children(
&[
Child::new(20, 1).divisible_by(3).weighted(2),
Child::new(3, 1).weighted(1),
Child::new(3, 1).weighted(1),
],
ConstraintLimit::Known(UPx(13)),
ConstraintLimit::ClippedAfter(UPx(13)),
&[UPx(6), UPx(3), UPx(4)],
UPx(13),
UPx(4), // 20 / 6 = 3.666, rounded up is 4
);
}
#[test]
fn fixed_size() {
assert_measured_children(
&[
Child::new(3, 1).fixed_size(UPx(7)),
Child::new(3, 1).weighted(1),
Child::new(3, 1).weighted(1),
],
ConstraintLimit::Known(UPx(15)),
ConstraintLimit::ClippedAfter(UPx(15)),
&[UPx(7), UPx(4), UPx(4)],
UPx(15),
UPx(1),
);
}
}

188
src/widgets/button.rs Normal file
View file

@ -0,0 +1,188 @@
use std::panic::UnwindSafe;
use kludgine::app::winit::event::{DeviceId, MouseButton};
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{Point, Rect, Size};
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::Color;
use crate::context::Context;
use crate::graphics::Graphics;
use crate::widget::{Callback, EventHandling, IntoValue, Value, Widget, HANDLED};
#[derive(Debug)]
pub struct Button {
pub label: Value<String>,
pub on_click: Option<Callback<()>>,
buttons_pressed: usize,
}
impl Button {
pub fn new(label: impl IntoValue<String>) -> Self {
Self {
label: label.into_value(),
on_click: None,
buttons_pressed: 0,
}
}
#[must_use]
pub fn on_click<F>(mut self, callback: F) -> Self
where
F: FnMut(()) + Send + UnwindSafe + 'static,
{
self.on_click = Some(Callback::new(callback));
self
}
}
impl Widget for Button {
fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context) {
let center = Point::from(graphics.size()) / 2;
if let Value::Dynamic(label) = &self.label {
context.redraw_when_changed(label);
}
let visible_rect = Rect::from(graphics.size() - (UPx(1), UPx(1)));
let background = if context.active() {
Color::new(30, 30, 30, 255)
} else if context.hovered() {
Color::new(40, 40, 40, 255)
} else {
Color::new(10, 10, 10, 255)
};
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());
graphics.draw_shape(&focus_ring, Point::default(), None, None);
}
let width = graphics.size().width;
self.label.map(|label| {
graphics.draw_text(
label,
Color::WHITE,
kludgine::text::TextOrigin::Center,
center,
None,
None,
Some(width),
);
});
}
fn hit_test(&mut self, _location: Point<Px>, _context: &mut Context<'_, '_>) -> bool {
true
}
fn mouse_down(
&mut self,
_location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
context: &mut Context<'_, '_>,
) -> EventHandling {
self.buttons_pressed += 1;
context.activate();
HANDLED
}
fn mouse_drag(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
context: &mut Context<'_, '_>,
) {
let changed = if Rect::from(
context
.last_rendered_at()
.expect("must have been rendered")
.size,
)
.contains(location)
{
context.activate()
} else {
context.deactivate()
};
if changed {
context.set_needs_redraw();
}
}
fn mouse_up(
&mut self,
location: Option<Point<Px>>,
_device_id: DeviceId,
_button: MouseButton,
context: &mut Context<'_, '_>,
) {
self.buttons_pressed -= 1;
if self.buttons_pressed == 0 {
context.deactivate();
if let Some(location) = location {
if Rect::from(
context
.last_rendered_at()
.expect("must have been rendered")
.size,
)
.contains(location)
{
context.focus();
if let Some(on_click) = self.on_click.as_mut() {
on_click.invoke(());
}
}
}
}
}
fn measure(
&mut self,
available_space: Size<crate::ConstraintLimit>,
graphics: &mut Graphics<'_, '_, '_>,
_context: &mut Context<'_, '_>,
) -> 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()
})
}
fn unhover(&mut self, context: &mut Context<'_, '_>) {
context.set_needs_redraw();
}
fn hover(&mut self, _location: Point<Px>, context: &mut Context<'_, '_>) {
context.set_needs_redraw();
}
fn focus(&mut self, context: &mut Context<'_, '_>) {
context.set_needs_redraw();
}
fn blur(&mut self, context: &mut Context<'_, '_>) {
context.set_needs_redraw();
}
fn activate(&mut self, context: &mut Context<'_, '_>) {
context.set_needs_redraw();
}
fn deactivate(&mut self, context: &mut Context<'_, '_>) {
context.set_needs_redraw();
}
}

90
src/widgets/canvas.rs Normal file
View file

@ -0,0 +1,90 @@
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::Context;
use crate::graphics::Graphics;
use crate::widget::Widget;
#[must_use]
pub struct Canvas {
render: Box<dyn RenderFunction>,
target_frame_duration: Option<Duration>,
last_frame_time: Option<Instant>,
}
impl Canvas {
pub fn new<F>(render: F) -> Self
where
F: for<'clip, 'gfx, 'pass, 'context, 'window> FnMut(
&mut Graphics<'clip, 'gfx, 'pass>,
&mut Context<'context, 'window>,
) + Send
+ UnwindSafe
+ 'static,
{
Self {
render: Box::new(render),
target_frame_duration: None,
last_frame_time: None,
}
}
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));
self
}
}
impl Widget for Canvas {
fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context<'_, '_>) {
self.render.render(graphics, 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);
}
}
fn measure(
&mut self,
available_space: Size<crate::ConstraintLimit>,
_graphics: &mut Graphics<'_, '_, '_>,
_context: &mut Context<'_, '_>,
) -> Size<UPx> {
Size::new(available_space.width.max(), available_space.height.max())
}
}
impl Debug for Canvas {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Canvas").finish_non_exhaustive()
}
}
trait RenderFunction: Send + UnwindSafe + 'static {
fn render(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context<'_, '_>);
}
impl<F> RenderFunction for F
where
F: for<'clip, 'gfx, 'pass, 'context, 'window> FnMut(
&mut Graphics<'clip, 'gfx, 'pass>,
&mut Context<'context, 'window>,
) + Send
+ UnwindSafe
+ 'static,
{
fn render(&mut self, graphics: &mut Graphics<'_, '_, '_>, window: &mut Context<'_, '_>) {
self(graphics, window);
}
}

58
src/widgets/label.rs Normal file
View file

@ -0,0 +1,58 @@
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{Point, Size};
use kludgine::text::TextOrigin;
use kludgine::Color;
use crate::context::Context;
use crate::graphics::Graphics;
use crate::widget::{IntoValue, Value, Widget};
#[derive(Debug)]
pub struct Label {
pub contents: Value<String>,
}
impl Label {
pub fn new(contents: impl IntoValue<String>) -> Self {
Self {
contents: contents.into_value(),
}
}
}
impl Widget for Label {
fn redraw(&mut self, graphics: &mut Graphics<'_, '_, '_>, context: &mut Context<'_, '_>) {
let center = Point::from(graphics.size()) / 2;
if let Value::Dynamic(contents) = &mut self.contents {
context.redraw_when_changed(contents);
}
let width = graphics.size().width;
self.contents.map(|contents| {
graphics.draw_text(
contents,
Color::RED,
TextOrigin::Center,
center,
None,
None,
Some(width),
);
});
}
fn measure(
&mut self,
available_space: Size<crate::ConstraintLimit>,
graphics: &mut Graphics<'_, '_, '_>,
_context: &mut Context<'_, '_>,
) -> Size<UPx> {
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
self.contents.map(|contents| {
graphics
.measure_text(contents, Color::RED, Some(width))
.size
.try_cast()
.unwrap_or_default()
})
}
}

424
src/window.rs Normal file
View file

@ -0,0 +1,424 @@
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, MouseButton};
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 crate::context::Context;
use crate::graphics::Graphics;
use crate::tree::{ManagedWidget, Tree};
use crate::utils::ModifiersExt;
use crate::widget::{EventHandling, HANDLED, UNHANDLED};
use crate::window::sealed::WindowCommand;
pub type RunningWindow<'window> = kludgine::app::Window<'window, WindowCommand>;
pub type WindowAttributes = kludgine::app::WindowAttributes<WindowCommand>;
pub struct Window<Behavior>
where
Behavior: WindowBehavior,
{
context: Behavior::Context,
pub attributes: WindowAttributes,
}
impl<Behavior> Default for Window<Behavior>
where
Behavior: WindowBehavior,
Behavior::Context: Default,
{
fn default() -> Self {
let context = Behavior::Context::default();
Self::new(context)
}
}
impl<Behavior> Window<Behavior>
where
Behavior: WindowBehavior,
{
pub fn new(context: Behavior::Context) -> Self {
Self {
attributes: WindowAttributes::default(),
context,
}
}
pub fn run(self) -> Result<(), EventLoopError> {
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe((
self.context,
RefCell::new(Some(self.attributes)),
)))
}
}
pub trait WindowBehavior: Sized + 'static {
type Context: UnwindSafe + Send + 'static;
fn initialize(window: &mut RunningWindow<'_>, context: Self::Context) -> Self;
fn make_root(&mut self, tree: &Tree) -> ManagedWidget;
#[allow(unused_variables)]
fn close_requested(&self, window: &mut RunningWindow<'_>) -> bool {
true
}
fn run() -> Result<(), EventLoopError>
where
Self::Context: Default,
{
Self::run_with(<Self::Context>::default())
}
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()
})),
)))
}
}
struct GooeyWindow<T> {
behavior: T,
root: ManagedWidget,
contents: Drawing,
should_close: bool,
mouse_state: MouseState,
}
impl<T> GooeyWindow<T>
where
T: WindowBehavior,
{
fn request_close(&mut self, window: &mut RunningWindow<'_>) -> bool {
self.should_close |= self.behavior.close_requested(window);
self.should_close
}
}
impl<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
where
T: WindowBehavior,
{
type Context = AssertUnwindSafe<(T::Context, RefCell<Option<WindowAttributes>>)>;
fn initialize(
mut window: RunningWindow<'_>,
_graphics: &mut kludgine::Graphics<'_>,
context: Self::Context,
) -> Self {
let mut behavior = T::initialize(&mut window, context.0 .0);
let root = behavior.make_root(&Tree::default());
Self {
behavior,
root,
contents: Drawing::default(),
should_close: false,
mouse_state: MouseState {
location: None,
widget: None,
devices: HashMap::default(),
},
}
}
fn prepare(&mut self, mut window: RunningWindow<'_>, graphics: &mut kludgine::Graphics<'_>) {
graphics.reset_text_attributes();
self.root.tree.reset_render_order();
let graphics = self.contents.new_frame(graphics);
let mut context = Context::new(&self.root, &mut window);
context.redraw(&mut Graphics::new(graphics));
}
fn render<'pass>(
&'pass mut self,
_window: RunningWindow<'_>,
graphics: &mut kludgine::RenderingGraphics<'_, 'pass>,
) -> bool {
self.contents.render(graphics);
!self.should_close
}
fn initial_window_attributes(context: &Self::Context) -> WindowAttributes {
context
.1
.borrow_mut()
.take()
.expect("called more than once")
}
fn close_requested(&mut self, mut window: RunningWindow<'_>) -> bool {
self.request_close(&mut window)
}
// fn power_preference() -> wgpu::PowerPreference {
// wgpu::PowerPreference::default()
// }
// fn limits(adapter_limits: wgpu::Limits) -> wgpu::Limits {
// wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits)
// }
// fn clear_color() -> Option<kludgine::Color> {
// Some(kludgine::Color::BLACK)
// }
// fn focus_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn occlusion_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn scale_factor_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn resized(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn theme_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn dropped_file(&mut self, window: kludgine::app::Window<'_, ()>, path: std::path::PathBuf) {}
// fn hovered_file(&mut self, window: kludgine::app::Window<'_, ()>, path: std::path::PathBuf) {}
// fn hovered_file_cancelled(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn received_character(&mut self, window: kludgine::app::Window<'_, ()>, char: char) {}
fn keyboard_input(
&mut self,
mut window: RunningWindow<'_>,
_device_id: DeviceId,
input: kludgine::app::winit::event::KeyEvent,
_is_synthetic: bool,
) {
if !input.state.is_pressed() {
match input.physical_key {
KeyCode::KeyW if window.modifiers().state().primary() => {
if self.request_close(&mut window) {
window.set_needs_redraw();
}
}
_ => {}
}
}
}
// fn modifiers_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
// fn ime(
// &mut self,
// window: kludgine::app::Window<'_, ()>,
// ime: kludgine::app::winit::event::Ime,
// ) {
// }
fn cursor_moved(
&mut self,
mut window: RunningWindow<'_>,
device_id: DeviceId,
position: PhysicalPosition<f64>,
) {
let location = Point::<Px>::from(position);
self.mouse_state.location = Some(location);
if let Some(state) = self.mouse_state.devices.get(&device_id) {
// Mouse Drag
for (button, handler) in state {
let mut context = Context::new(handler, &mut window);
let last_rendered_at = context.last_rendered_at().expect("passed hit test");
context.mouse_drag(location - last_rendered_at.origin, device_id, *button);
}
} else {
// Hover
let mut context = Context::new(&self.root, &mut window);
for widget in self.root.tree.widgets_at_point(location) {
let mut widget_context = context.for_other(&widget);
let relative = location
- widget_context
.last_rendered_at()
.expect("passed hit test")
.origin;
if widget_context.hit_test(relative) {
widget_context.hover(relative);
drop(widget_context);
self.mouse_state.widget = Some(widget);
break;
}
}
}
}
// 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);
context.clear_hover();
}
}
// 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<'_>,
device_id: DeviceId,
state: ElementState,
button: MouseButton,
) {
match state {
ElementState::Pressed => {
Context::new(&self.root, &mut window).clear_focus();
if let (ElementState::Pressed, Some(location), Some(hovered)) =
(state, &self.mouse_state.location, &self.mouse_state.widget)
{
if let Some(handler) = recursively_handle_event(
&mut Context::new(hovered, &mut window),
|context| {
let relative = *location
- context.last_rendered_at().expect("passed hit test").origin;
context.mouse_down(relative, device_id, button)
},
) {
self.mouse_state
.devices
.entry(device_id)
.or_default()
.insert(button, handler);
}
}
}
ElementState::Released => {
let Some(device_buttons) = self.mouse_state.devices.get_mut(&device_id) else {
return;
};
let Some(handler) = device_buttons.remove(&button) else {
return;
};
if device_buttons.is_empty() {
self.mouse_state.devices.remove(&device_id);
}
let mut context = Context::new(&handler, &mut window);
let relative = if let (Some(last_rendered), Some(location)) =
(context.last_rendered_at(), self.mouse_state.location)
{
Some(location - last_rendered.origin)
} else {
None
};
context.mouse_up(relative, device_id, button);
}
}
}
// 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 => {
window.set_needs_redraw();
}
}
}
}
fn recursively_handle_event(
context: &mut Context<'_, '_>,
mut each_widget: impl FnMut(&mut Context<'_, '_>) -> EventHandling,
) -> Option<ManagedWidget> {
match each_widget(context) {
HANDLED => Some(context.widget().clone()),
UNHANDLED => context.parent().and_then(|parent| {
recursively_handle_event(&mut context.for_other(&parent), each_widget)
}),
}
}
#[derive(Default)]
struct MouseState {
location: Option<Point<Px>>,
widget: Option<ManagedWidget>,
devices: HashMap<DeviceId, HashMap<MouseButton, ManagedWidget>>,
}
pub(crate) mod sealed {
pub enum WindowCommand {
Redraw,
// RequestClose,
}
}