mirror of
https://github.com/danbulant/cushy
synced 2026-06-20 15:01:11 +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