Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep hot state consistent with mouse position. #841

Merged
merged 5 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 74 additions & 20 deletions druid/examples/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,63 +16,117 @@

use std::time::{Duration, Instant};

use druid::kurbo::Line;
use druid::widget::prelude::*;
use druid::{AppLauncher, Color, LocalizedString, TimerToken, WindowDesc};
use druid::widget::BackgroundBrush;
use druid::{AppLauncher, Color, LocalizedString, Point, Rect, TimerToken, WidgetPod, WindowDesc};

static TIMER_INTERVAL: u64 = 10;

struct TimerWidget {
timer_id: TimerToken,
on: bool,
simple_box: WidgetPod<u32, SimpleBox>,
pos: Point,
}

impl TimerWidget {
/// Move the box towards the right, until it reaches the edge,
/// then reset it to the left but move it to another row.
fn adjust_box_pos(&mut self, container_size: Size) {
let box_size = self.simple_box.layout_rect().size();
self.pos.x += 2.;
if self.pos.x + box_size.width > container_size.width {
self.pos.x = 0.;
self.pos.y += box_size.height;
if self.pos.y + box_size.height > container_size.height {
self.pos.y = 0.;
}
}
}
}

impl Widget<u32> for TimerWidget {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut u32, _env: &Env) {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut u32, env: &Env) {
match event {
Event::MouseDown(_) => {
self.on = !self.on;
ctx.request_paint();
let deadline = Instant::now() + Duration::from_millis(500);
Event::WindowConnected => {
// Start the timer when the application launches
let deadline = Instant::now() + Duration::from_millis(TIMER_INTERVAL);
self.timer_id = ctx.request_timer(deadline);
}
Event::Timer(id) => {
if *id == self.timer_id {
self.on = !self.on;
ctx.request_paint();
let deadline = Instant::now() + Duration::from_millis(500);
self.adjust_box_pos(ctx.size());
ctx.request_layout();
let deadline = Instant::now() + Duration::from_millis(TIMER_INTERVAL);
self.timer_id = ctx.request_timer(deadline);
}
}
_ => (),
}
self.simple_box.event(ctx, event, data, env);
}

fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &u32, _env: &Env) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &u32, env: &Env) {
self.simple_box.lifecycle(ctx, event, data, env);
}

fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &u32, data: &u32, env: &Env) {
self.simple_box.update(ctx, data, env);
}

fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &u32, env: &Env) -> Size {
let size = self.simple_box.layout(ctx, &bc.loosen(), data, env);
let rect = Rect::from_origin_size(self.pos, size);
self.simple_box.set_layout_rect(ctx, data, env, rect);
bc.constrain((500.0, 500.0))
}

fn paint(&mut self, ctx: &mut PaintCtx, data: &u32, env: &Env) {
self.simple_box.paint_with_offset(ctx, data, env);
}
}

struct SimpleBox;

impl Widget<u32> for SimpleBox {
fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut u32, _env: &Env) {}

fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &u32, _env: &Env) {
match _event {
LifeCycle::HotChanged(_) => ctx.request_paint(),
_ => (),
}
}

fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &u32, _data: &u32, _env: &Env) {}

fn layout(
&mut self,
_layout_ctx: &mut LayoutCtx,
_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &u32,
_env: &Env,
) -> Size {
bc.constrain((100.0, 100.0))
bc.constrain((50.0, 50.0))
}

fn paint(&mut self, ctx: &mut PaintCtx, _data: &u32, _env: &Env) {
if self.on {
ctx.stroke(Line::new((10.0, 10.0), (10.0, 50.0)), &Color::WHITE, 1.0);
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &u32, env: &Env) {
let mut background = if ctx.is_hot() {
BackgroundBrush::Color(Color::rgb(255, 0, 0))
} else {
BackgroundBrush::Color(Color::rgb(0, 255, 255))
};
background.paint(ctx, data, env);
}
}

fn main() {
let window = WindowDesc::new(|| TimerWidget {
timer_id: TimerToken::INVALID,
on: false,
simple_box: WidgetPod::new(SimpleBox),
pos: Point::ZERO,
})
.title(LocalizedString::new("timer-demo-window-title").with_placeholder("Tick Tock"));
.with_min_size((200., 200.))
.title(LocalizedString::new("timer-demo-window-title").with_placeholder("Look at it go!"));

AppLauncher::with_window(window)
.use_simple_logger()
Expand Down
10 changes: 6 additions & 4 deletions druid/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use crate::core::{BaseState, CommandQueue, FocusChange};
use crate::piet::Piet;
use crate::piet::RenderContext;
use crate::{
Affine, Command, Cursor, Insets, Rect, Size, Target, Text, TimerToken, WidgetId, WindowHandle,
WindowId,
Affine, Command, Cursor, Insets, Point, Rect, Size, Target, Text, TimerToken, WidgetId,
WindowHandle, WindowId,
};

/// A mutable context provided to event handling methods of widgets.
Expand Down Expand Up @@ -85,9 +85,11 @@ pub struct UpdateCtx<'a> {
/// creating text layout objects, which are likely to be useful
/// during widget layout.
pub struct LayoutCtx<'a, 'b: 'a> {
pub(crate) command_queue: &'a mut CommandQueue,
pub(crate) base_state: &'a mut BaseState,
pub(crate) text_factory: &'a mut Text<'b>,
pub(crate) paint_insets: Insets,
pub(crate) window_id: WindowId,
pub(crate) mouse_pos: Option<Point>,
}

/// Z-order paint operations with transformations.
Expand Down Expand Up @@ -551,7 +553,7 @@ impl<'a, 'b> LayoutCtx<'a, 'b> {
/// [`Insets`]: struct.Insets.html
/// [`WidgetPod::paint_insets`]: struct.WidgetPod.html#method.paint_insets
pub fn set_paint_insets(&mut self, insets: impl Into<Insets>) {
self.paint_insets = insets.into().nonnegative();
self.base_state.paint_insets = insets.into().nonnegative();
}
}

Expand Down
53 changes: 40 additions & 13 deletions druid/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub(crate) struct BaseState {
/// The insets applied to the layout rect to generate the paint rect.
/// In general, these will be zero; the exception is for things like
/// drop shadows or overflowing text.
paint_insets: Insets,
pub(crate) paint_insets: Insets,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might make sense to have the 'last mouse position' also be in base state? I could imagine it being useful for lots of things, like calculating deltas when you see MouseMoved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having it in base state wouldn't help with this particular PR, because the base state value is going to be stale in layout. This could be observed in the timer example but it's not limited to that.

If we end up wanting the last position in base state, then this layout pass would just be another update source for the base state's cached value.

Given this, I think it's best to go with YAGNI here. When we eventually need it in base state, then that's the moment to add it.


// TODO: consider using bitflags for the booleans.

Expand Down Expand Up @@ -182,8 +182,25 @@ impl<T, W: Widget<T>> WidgetPod<T, W> {
///
/// Intended to be called on child widget in container's `layout`
/// implementation.
pub fn set_layout_rect(&mut self, layout_rect: Rect) {
pub fn set_layout_rect(&mut self, ctx: &mut LayoutCtx, data: &T, env: &Env, layout_rect: Rect) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish there were an alternative, but I can't think of one.

self.state.layout_rect = Some(layout_rect);

let had_hot = self.state.is_hot;
self.state.is_hot = match ctx.mouse_pos {
Some(pos) => layout_rect.winding(pos) != 0,
None => false,
};
if had_hot != self.state.is_hot {
let hot_changed_event = LifeCycle::HotChanged(self.state.is_hot);
let mut child_ctx = LifeCycleCtx {
command_queue: ctx.command_queue,
base_state: &mut self.state,
window_id: ctx.window_id,
};
self.inner
.lifecycle(&mut child_ctx, &hot_changed_event, data, env);
ctx.base_state.merge_up(&child_ctx.base_state);
}
}

#[deprecated(since = "0.5.0", note = "use layout_rect() instead")]
Expand Down Expand Up @@ -268,7 +285,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
base_state: &self.state,
focus_widget: ctx.focus_widget,
};
self.inner.paint(&mut inner_ctx, data, &env);
self.inner.paint(&mut inner_ctx, data, env);
ctx.z_ops.append(&mut inner_ctx.z_ops);

if env.get(Env::DEBUG_PAINT) {
Expand Down Expand Up @@ -313,7 +330,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
let layout_origin = self.layout_rect().origin().to_vec2();
ctx.transform(Affine::translate(layout_origin));
let visible = ctx.region().to_rect() - layout_origin;
ctx.with_child_ctx(visible, |ctx| self.paint(ctx, data, &env));
ctx.with_child_ctx(visible, |ctx| self.paint(ctx, data, env));
});
}

Expand All @@ -325,26 +342,36 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
/// [`layout`]: trait.Widget.html#tymethod.layout
pub fn layout(
&mut self,
layout_ctx: &mut LayoutCtx,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &T,
env: &Env,
) -> Size {
layout_ctx.paint_insets = Insets::ZERO;
let size = self.inner.layout(layout_ctx, bc, data, &env);
self.state.needs_layout = false;

let child_mouse_pos = match ctx.mouse_pos {
Some(pos) => Some(pos - self.layout_rect().origin().to_vec2()),
None => None,
};
let mut child_ctx = LayoutCtx {
command_queue: ctx.command_queue,
base_state: &mut self.state,
window_id: ctx.window_id,
text_factory: ctx.text_factory,
mouse_pos: child_mouse_pos,
};
let size = self.inner.layout(&mut child_ctx, bc, data, env);

ctx.base_state.merge_up(&child_ctx.base_state);

if size.width.is_infinite() {
let name = self.widget().type_name();
log::warn!("Widget `{}` has an infinite width.", name);
}

if size.height.is_infinite() {
let name = self.widget().type_name();
log::warn!("Widget `{}` has an infinite height.", name);
}

self.state.paint_insets = layout_ctx.paint_insets;
self.state.needs_layout = false;
size
}

Expand Down Expand Up @@ -496,11 +523,11 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
let hot_changed_event = LifeCycle::HotChanged(is_hot);
let mut lc_ctx = child_ctx.make_lifecycle_ctx();
self.inner
.lifecycle(&mut lc_ctx, &hot_changed_event, data, &env);
.lifecycle(&mut lc_ctx, &hot_changed_event, data, env);
}
if recurse {
child_ctx.base_state.has_active = false;
self.inner.event(&mut child_ctx, &child_event, data, &env);
self.inner.event(&mut child_ctx, &child_event, data, env);
child_ctx.base_state.has_active |= child_ctx.base_state.is_active;
};

Expand Down
7 changes: 4 additions & 3 deletions druid/src/tests/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,16 @@ impl<T: Data> Inner<T> {

fn lifecycle(&mut self, event: LifeCycle) {
self.window
.lifecycle(&mut self.cmds, &event, &self.data, &self.env);
.lifecycle(&mut self.cmds, &event, &self.data, &self.env, false);
}

fn update(&mut self) {
self.window.update(&self.data, &self.env);
self.window.update(&mut self.cmds, &self.data, &self.env);
}

fn layout(&mut self, piet: &mut Piet) {
self.window.just_layout(piet, &self.data, &self.env);
self.window
.just_layout(piet, &mut self.cmds, &self.data, &self.env);
}

#[allow(dead_code)]
Expand Down
14 changes: 4 additions & 10 deletions druid/src/widget/align.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,10 @@ impl<T: Data> Widget<T> for Align<T> {
self.child.update(ctx, data, env);
}

fn layout(
&mut self,
layout_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &T,
env: &Env,
) -> Size {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
bc.debug_check("Align");

let size = self.child.layout(layout_ctx, &bc.loosen(), data, env);
let size = self.child.layout(ctx, &bc.loosen(), data, env);

log_size_warnings(size);

Expand All @@ -129,10 +123,10 @@ impl<T: Data> Widget<T> for Align<T> {
.align
.resolve(Rect::new(0., 0., extra_width, extra_height));
self.child
.set_layout_rect(Rect::from_origin_size(origin, size));
.set_layout_rect(ctx, data, env, Rect::from_origin_size(origin, size));

let my_insets = self.child.compute_parent_paint_insets(my_size);
layout_ctx.set_paint_insets(my_insets);
ctx.set_paint_insets(my_insets);
my_size
}

Expand Down
18 changes: 8 additions & 10 deletions druid/src/widget/checkbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,20 @@ impl Widget<bool> for Checkbox {
ctx.request_paint();
}

fn layout(
&mut self,
layout_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &bool,
env: &Env,
) -> Size {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &bool, env: &Env) -> Size {
bc.debug_check("Checkbox");

let label_size = self.child_label.layout(layout_ctx, &bc, data, env);
let label_size = self.child_label.layout(ctx, &bc, data, env);
let padding = 8.0;
let label_x_offset = env.get(theme::BASIC_WIDGET_HEIGHT) + padding;
let origin = Point::new(label_x_offset, 0.0);

self.child_label
.set_layout_rect(Rect::from_origin_size(origin, label_size));
self.child_label.set_layout_rect(
ctx,
data,
env,
Rect::from_origin_size(origin, label_size),
);

bc.constrain(Size::new(
label_x_offset + label_size.width,
Expand Down
2 changes: 1 addition & 1 deletion druid/src/widget/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl<T: Data> Widget<T> for Container<T> {
let size = self.inner.layout(ctx, &child_bc, data, env);
let origin = Point::new(border_width, border_width);
self.inner
.set_layout_rect(Rect::from_origin_size(origin, size));
.set_layout_rect(ctx, data, env, Rect::from_origin_size(origin, size));

let my_size = Size::new(
size.width + 2.0 * border_width,
Expand Down
Loading