Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e97aba30e0 | ||
|
|
32b10383d8 | ||
|
|
d5efdd2b97 | ||
|
|
00d2230b84 |
No files matched your search
@@ -34,6 +34,10 @@ impl UiRenderState {
|
|||||||
self.resized = true;
|
self.resized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_size(&self) -> Vec2 {
|
||||||
|
self.output_size
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
||||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||||
// decide whether to panic or not
|
// decide whether to panic or not
|
||||||
|
|||||||
+9
-7
@@ -1,10 +1,6 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use std::{
|
use std::{marker::PhantomData, sync::Arc, time::Instant};
|
||||||
marker::{PhantomData, Sized},
|
|
||||||
sync::Arc,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
use winit::{
|
use winit::{
|
||||||
event::{Ime, WindowEvent},
|
event::{Ime, WindowEvent},
|
||||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||||
@@ -64,6 +60,12 @@ impl DefaultUiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WakeTaskQueue for Window {
|
||||||
|
fn wake(&self) {
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait HasDefaultUiState: Sized + 'static {
|
pub trait HasDefaultUiState: Sized + 'static {
|
||||||
fn default_state(&self) -> &DefaultUiState;
|
fn default_state(&self) -> &DefaultUiState;
|
||||||
fn default_state_mut(&mut self) -> &mut DefaultUiState;
|
fn default_state_mut(&mut self) -> &mut DefaultUiState;
|
||||||
@@ -105,8 +107,8 @@ pub struct DefaultRsc<State: 'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<State> DefaultRsc<State> {
|
impl<State> DefaultRsc<State> {
|
||||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
|
||||||
let (tasks, recv) = Tasks::init(window);
|
let (tasks, recv) = Tasks::init(wake);
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
ui: Default::default(),
|
ui: Default::default(),
|
||||||
|
|||||||
+63
-10
@@ -13,7 +13,13 @@ use tokio::{
|
|||||||
unbounded_channel as async_channel,
|
unbounded_channel as async_channel,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use winit::window::Window;
|
|
||||||
|
/// Wakes the host so it applies queued task updates. A task reaches the
|
||||||
|
/// application only through [`TaskCtx::update`], so this is all the queue
|
||||||
|
/// needs of a platform.
|
||||||
|
pub trait WakeTaskQueue: Send + Sync + 'static {
|
||||||
|
fn wake(&self);
|
||||||
|
}
|
||||||
|
|
||||||
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
||||||
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
||||||
@@ -23,29 +29,32 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
|
|||||||
|
|
||||||
pub struct Tasks<Rsc: HasState> {
|
pub struct Tasks<Rsc: HasState> {
|
||||||
start: AsyncSender<BoxTask>,
|
start: AsyncSender<BoxTask>,
|
||||||
window: Arc<Window>,
|
wake: Arc<dyn WakeTaskQueue>,
|
||||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TaskCtx<Rsc: HasState> {
|
pub struct TaskCtx<Rsc: HasState> {
|
||||||
send: TaskMsgSender<Rsc>,
|
send: TaskMsgSender<Rsc>,
|
||||||
|
wake: Arc<dyn WakeTaskQueue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: HasState> TaskCtx<Rsc> {
|
impl<Rsc: HasState> TaskCtx<Rsc> {
|
||||||
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
||||||
let _ = self.send.send(Box::new(f));
|
if self.send.send(Box::new(f)).is_ok() {
|
||||||
|
self.wake.wake();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
||||||
fn new(send: TaskMsgSender<Rsc>) -> Self {
|
fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self {
|
||||||
Self { send }
|
Self { send, wake }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||||
|
|
||||||
impl<Rsc: HasState> Tasks<Rsc> {
|
impl<Rsc: HasState> Tasks<Rsc> {
|
||||||
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
|
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||||
let (start, start_recv) = async_channel();
|
let (start, start_recv) = async_channel();
|
||||||
let (msgs, msgs_recv) = sync_channel();
|
let (msgs, msgs_recv) = sync_channel();
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
@@ -56,7 +65,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
|||||||
Self {
|
Self {
|
||||||
start,
|
start,
|
||||||
msg_send: msgs,
|
msg_send: msgs,
|
||||||
window,
|
wake,
|
||||||
},
|
},
|
||||||
msgs_recv,
|
msgs_recv,
|
||||||
)
|
)
|
||||||
@@ -67,10 +76,9 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
|||||||
F::CallOnceFuture: Send,
|
F::CallOnceFuture: Send,
|
||||||
{
|
{
|
||||||
let send = self.msg_send.clone();
|
let send = self.msg_send.clone();
|
||||||
let window = self.window.clone();
|
let wake = self.wake.clone();
|
||||||
let _ = self.start.send(Box::pin(async move {
|
let _ = self.start.send(Box::pin(async move {
|
||||||
task(TaskCtx::new(send)).await;
|
task(TaskCtx::new(send, wake)).await;
|
||||||
window.request_redraw();
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,3 +88,48 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
|
|||||||
tokio::spawn(task);
|
tokio::spawn(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::{sync::mpsc::sync_channel, time::Duration};
|
||||||
|
|
||||||
|
struct TestRsc;
|
||||||
|
|
||||||
|
impl HasState for TestRsc {
|
||||||
|
type State = usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals rather than counts, so the test waits for a wake instead of
|
||||||
|
/// racing the task thread to sample it.
|
||||||
|
struct WakeSignal(std::sync::mpsc::SyncSender<()>);
|
||||||
|
|
||||||
|
impl WakeTaskQueue for WakeSignal {
|
||||||
|
fn wake(&self) {
|
||||||
|
let _ = self.0.send(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_update_wakes_the_host() {
|
||||||
|
let (woken, wakes) = sync_channel(8);
|
||||||
|
let (mut tasks, updates) = Tasks::<TestRsc>::init(Arc::new(WakeSignal(woken)));
|
||||||
|
|
||||||
|
tasks.spawn(async move |mut ctx| {
|
||||||
|
ctx.update(|state: &mut usize, _| *state += 1);
|
||||||
|
ctx.update(|state: &mut usize, _| *state += 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
let second = Duration::from_secs(1);
|
||||||
|
let (mut state, mut rsc) = (0, TestRsc);
|
||||||
|
for _ in 0..2 {
|
||||||
|
wakes.recv_timeout(second).expect("no wake for an update");
|
||||||
|
updates.recv_timeout(second).unwrap()(&mut state, &mut rsc);
|
||||||
|
}
|
||||||
|
assert_eq!(state, 3);
|
||||||
|
assert!(
|
||||||
|
wakes.recv_timeout(Duration::from_millis(100)).is_err(),
|
||||||
|
"woken with nothing to apply"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
//! A ui with no window: build a tree, run frames, move a pointer, and read
|
||||||
|
//! back where widgets landed.
|
||||||
|
//!
|
||||||
|
//! It does not draw. A claim about pixels still needs a real surface.
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
/// The harness drains the update queue itself, so there is no loop to wake.
|
||||||
|
struct NoWake;
|
||||||
|
|
||||||
|
impl WakeTaskQueue for NoWake {
|
||||||
|
fn wake(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct HarnessState {
|
||||||
|
pub root: Option<StrongWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasRoot for HarnessState {
|
||||||
|
fn set_root(&mut self, root: StrongWidget) {
|
||||||
|
self.root = Some(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Harness {
|
||||||
|
pub rsc: DefaultRsc<HarnessState>,
|
||||||
|
pub render: UiRenderState,
|
||||||
|
pub state: HarnessState,
|
||||||
|
updates: TaskMsgReceiver<DefaultRsc<HarnessState>>,
|
||||||
|
cursor: CursorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Harness {
|
||||||
|
/// `size` is the output in physical pixels.
|
||||||
|
pub fn new(size: impl Into<Vec2>) -> Self {
|
||||||
|
let (rsc, updates) = DefaultRsc::init(Arc::new(NoWake));
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize(size);
|
||||||
|
Self {
|
||||||
|
rsc,
|
||||||
|
render,
|
||||||
|
state: HarnessState::default(),
|
||||||
|
updates,
|
||||||
|
cursor: CursorState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> Vec2 {
|
||||||
|
self.render.output_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
|
self.render.resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||||
|
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
||||||
|
widget.set_root(&mut self.rsc, &mut self.state);
|
||||||
|
self.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_redraw(&self) -> bool {
|
||||||
|
self.render
|
||||||
|
.needs_redraw(&self.state.root, self.rsc.widgets())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_updates(&mut self) -> usize {
|
||||||
|
let mut applied = 0;
|
||||||
|
while let Ok(update) = self.updates.try_recv() {
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
applied += 1;
|
||||||
|
}
|
||||||
|
applied
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for a task's first update, then applies everything waiting.
|
||||||
|
/// False if none arrived in time.
|
||||||
|
#[must_use]
|
||||||
|
pub fn await_update(&mut self, timeout: Duration) -> bool {
|
||||||
|
let Ok(update) = self.updates.recv_timeout(timeout) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
self.apply_updates();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays the tree out and builds its primitives.
|
||||||
|
pub fn frame(&mut self) {
|
||||||
|
self.apply_updates();
|
||||||
|
self.render.update(&self.state.root, &mut self.rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the last frame put a widget, or `None` if it drew nothing.
|
||||||
|
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
|
self.render.window_region(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
|
||||||
|
self.cursor.pos = pos.into();
|
||||||
|
self.cursor.exists = true;
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(&mut self) {
|
||||||
|
self.cursor.exists = false;
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press(&mut self, button: CursorButton) {
|
||||||
|
self.button(button).update(true);
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(&mut self, button: CursorButton) {
|
||||||
|
self.button(button).update(false);
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wheel carries no position, so this goes wherever
|
||||||
|
/// [`move_to`](Self::move_to) last put the cursor -- nowhere, until it has
|
||||||
|
/// been called.
|
||||||
|
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
|
||||||
|
self.cursor.scroll_delta = delta.into();
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn click(&mut self, pos: impl Into<Vec2>) {
|
||||||
|
self.move_to(pos);
|
||||||
|
self.press(CursorButton::Left);
|
||||||
|
self.release(CursorButton::Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
|
||||||
|
let buttons = &mut self.cursor.buttons;
|
||||||
|
match button {
|
||||||
|
CursorButton::Left => &mut buttons.left,
|
||||||
|
CursorButton::Middle => &mut buttons.middle,
|
||||||
|
CursorButton::Right => &mut buttons.right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches against the last [`frame`](Self::frame)'s layout, which is
|
||||||
|
/// what a window delivers input against too.
|
||||||
|
fn sense(&mut self) {
|
||||||
|
let cursor = self.cursor.clone();
|
||||||
|
let size = self.render.output_size();
|
||||||
|
self.render
|
||||||
|
.run_sensors(&mut self.rsc, &mut self.state, cursor, size);
|
||||||
|
self.cursor.end_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
pub mod default;
|
pub mod default;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod harness;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
pub use iris_core as core;
|
pub use iris_core as core;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ mod max_size;
|
|||||||
mod offset;
|
mod offset;
|
||||||
mod pad;
|
mod pad;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod sized;
|
mod set_size;
|
||||||
mod span;
|
mod span;
|
||||||
mod stack;
|
mod stack;
|
||||||
|
|
||||||
@@ -14,6 +14,6 @@ pub use max_size::*;
|
|||||||
pub use offset::*;
|
pub use offset::*;
|
||||||
pub use pad::*;
|
pub use pad::*;
|
||||||
pub use scroll::*;
|
pub use scroll::*;
|
||||||
pub use sized::*;
|
pub use set_size::*;
|
||||||
pub use span::*;
|
pub use span::*;
|
||||||
pub use stack::*;
|
pub use stack::*;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub struct Sized {
|
pub struct SetSize {
|
||||||
pub inner: StrongWidget,
|
pub inner: StrongWidget,
|
||||||
pub x: Option<Len>,
|
pub x: Option<Len>,
|
||||||
pub y: Option<Len>,
|
pub y: Option<Len>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sized {
|
impl SetSize {
|
||||||
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
||||||
if let Some(x) = self.x {
|
if let Some(x) = self.x {
|
||||||
ctx.outer.x.select_len(x.apply_rest());
|
ctx.outer.x.select_len(x.apply_rest());
|
||||||
@@ -17,7 +17,7 @@ impl Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Sized {
|
impl Widget for SetSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
painter.widget(&self.inner);
|
painter.widget(&self.inner);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::marker::{Sized, Unsize};
|
use std::marker::Unsize;
|
||||||
|
|
||||||
pub struct WidgetPtr {
|
pub struct WidgetPtr {
|
||||||
pub inner: Option<StrongWidget>,
|
pub inner: Option<StrongWidget>,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::marker::{PhantomData, Sized};
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
|
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
|
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
move |state| Sized {
|
move |state| SetSize {
|
||||||
inner: self.add_strong(state),
|
inner: self.add_strong(state),
|
||||||
x: Some(size.x),
|
x: Some(size.x),
|
||||||
y: Some(size.y),
|
y: Some(size.y),
|
||||||
@@ -58,18 +58,18 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
|
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| Sized {
|
move |state| SetSize {
|
||||||
inner: self.add_strong(state),
|
inner: self.add_strong(state),
|
||||||
x: Some(len),
|
x: Some(len),
|
||||||
y: None,
|
y: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
|
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| Sized {
|
move |state| SetSize {
|
||||||
inner: self.add_strong(state),
|
inner: self.add_strong(state),
|
||||||
x: None,
|
x: None,
|
||||||
y: Some(len),
|
y: Some(len),
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//! Layout, hit testing and task updates, driven without a window.
|
||||||
|
|
||||||
|
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// `PixelRegion` neither compares nor prints.
|
||||||
|
fn corners(h: &Harness, id: &impl IdLike) -> (f32, f32, f32, f32) {
|
||||||
|
let region = h.region(id).expect("widget drew nothing");
|
||||||
|
(
|
||||||
|
region.top_left.x,
|
||||||
|
region.top_left.y,
|
||||||
|
region.bot_right.x,
|
||||||
|
region.bot_right.y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
|
||||||
|
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
|
||||||
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
(left.id(), right.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_gives_each_child_the_width_it_asked_for() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (left, right) = two_rects(&mut h);
|
||||||
|
|
||||||
|
assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 200.0));
|
||||||
|
assert_eq!(corners(&h, &right), (100.0, 0.0, 400.0, 200.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resizing_relays_out_against_the_new_output() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (left, right) = two_rects(&mut h);
|
||||||
|
|
||||||
|
h.resize((800, 100));
|
||||||
|
assert!(h.needs_redraw());
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 100.0));
|
||||||
|
assert_eq!(corners(&h, &right), (100.0, 0.0, 800.0, 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_press_reaches_only_the_widget_under_the_cursor() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let clicks = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
|
||||||
|
let (on_left, on_right) = (clicks.clone(), clicks.clone());
|
||||||
|
let left = rect(Color::RED)
|
||||||
|
.width(100)
|
||||||
|
.on(CursorSense::click(), move |_, _| {
|
||||||
|
on_left.borrow_mut().push("left")
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE)
|
||||||
|
.on(CursorSense::click(), move |_, _| {
|
||||||
|
on_right.borrow_mut().push("right")
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
h.click((50, 100));
|
||||||
|
assert_eq!(*clicks.borrow(), ["left"]);
|
||||||
|
|
||||||
|
h.click((300, 100));
|
||||||
|
assert_eq!(*clicks.borrow(), ["left", "right"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_ends_when_the_cursor_leaves_the_window() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let hovered = Rc::new(RefCell::new(0));
|
||||||
|
let ended = Rc::new(RefCell::new(0));
|
||||||
|
|
||||||
|
let (h_count, e_count) = (hovered.clone(), ended.clone());
|
||||||
|
let widget = rect(Color::RED)
|
||||||
|
.on(CursorSense::HoverStart, move |_, _| {
|
||||||
|
*h_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.on(CursorSense::HoverEnd, move |_, _| {
|
||||||
|
*e_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(widget);
|
||||||
|
|
||||||
|
h.move_to((200, 100));
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||||
|
|
||||||
|
// A second sample inside the same widget is not a second hover.
|
||||||
|
h.move_to((210, 100));
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||||
|
|
||||||
|
h.leave();
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_task_update_reaches_the_tree() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let widget = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
|
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
|
||||||
|
}));
|
||||||
|
|
||||||
|
h.click((200, 100));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
h.await_update(Duration::from_secs(5)),
|
||||||
|
"the task sent no update"
|
||||||
|
);
|
||||||
|
assert_eq!(h.rsc[widget].color, Color::BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// Twice the window's height, so there is 200 to scroll.
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
||||||
|
h.move_to((200, 100));
|
||||||
|
|
||||||
|
// `Scroll` starts snapped to the end.
|
||||||
|
assert_eq!(corners(&h, &top).1, -200.0);
|
||||||
|
|
||||||
|
// The handler scales a wheel line by 50.
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(corners(&h, &top).1, -150.0);
|
||||||
|
|
||||||
|
h.scroll((0, 10));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(corners(&h, &top).1, 0.0);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user