mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 19:50:40 +00:00
Initial commit
This commit is contained in:
commit
fc707835f5
21 changed files with 4978 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
target/
|
||||||
2259
Cargo.lock
generated
Normal file
2259
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
11
examples/button.rs
Normal 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
35
examples/canvas.rs
Normal 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
28
examples/counter.rs
Normal 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
6
rustfmt.toml
Normal 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
69
src/children.rs
Normal 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
313
src/context.rs
Normal 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
307
src/dynamic.rs
Normal 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
48
src/graphics.rs
Normal 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
37
src/lib.rs
Normal 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
251
src/tree.rs
Normal 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
17
src/utils.rs
Normal 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
235
src/widget.rs
Normal 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
8
src/widgets.rs
Normal 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
575
src/widgets/array.rs
Normal 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
188
src/widgets/button.rs
Normal 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
90
src/widgets/canvas.rs
Normal 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
58
src/widgets/label.rs
Normal 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
424
src/window.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue