use std::collections::{HashMap, VecDeque};
use crate::bloom::Bloom;
use crate::contexts::ContextState;
use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2};
use crate::piet::{
FontBuilder, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder,
};
use crate::util::ExtendDrain;
use crate::{
BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle,
LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, Target, TimerToken, UpdateCtx, Widget,
WidgetId,
};
pub(crate) type CommandQueue = VecDeque<(Target, Command)>;
pub struct WidgetPod<T, W> {
state: WidgetState,
old_data: Option<T>,
env: Option<Env>,
inner: W,
debug_widget_text: Option<PietTextLayout>,
}
#[derive(Clone)]
pub(crate) struct WidgetState {
pub(crate) id: WidgetId,
layout_rect: Option<Rect>,
pub(crate) paint_insets: Insets,
pub(crate) invalid: Region,
pub(crate) viewport_offset: Vec2,
pub(crate) is_hot: bool,
pub(crate) is_active: bool,
pub(crate) needs_layout: bool,
has_active: bool,
pub(crate) has_focus: bool,
pub(crate) request_anim: bool,
pub(crate) focus_chain: Vec<WidgetId>,
pub(crate) request_focus: Option<FocusChange>,
pub(crate) children: Bloom<WidgetId>,
pub(crate) children_changed: bool,
pub(crate) timers: HashMap<TimerToken, WidgetId>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum FocusChange {
Resign,
Focus(WidgetId),
Next,
Previous,
}
impl<T, W: Widget<T>> WidgetPod<T, W> {
pub fn new(inner: W) -> WidgetPod<T, W> {
let mut state = WidgetState::new(inner.id().unwrap_or_else(WidgetId::next));
state.children_changed = true;
state.needs_layout = true;
WidgetPod {
state,
old_data: None,
env: None,
inner,
debug_widget_text: None,
}
}
pub(crate) fn state(&self) -> &WidgetState {
&self.state
}
pub fn is_active(&self) -> bool {
self.state.is_active
}
pub fn has_active(&self) -> bool {
self.state.has_active
}
pub fn is_hot(&self) -> bool {
self.state.is_hot
}
pub fn widget(&self) -> &W {
&self.inner
}
pub fn widget_mut(&mut self) -> &mut W {
&mut self.inner
}
pub fn id(&self) -> WidgetId {
self.state.id
}
pub fn set_layout_rect(&mut self, ctx: &mut LayoutCtx, data: &T, env: &Env, layout_rect: Rect) {
let mut needs_merge = false;
let old_size = self.state.layout_rect.map(|r| r.size());
let new_size = layout_rect.size();
self.state.layout_rect = Some(layout_rect);
if old_size.is_none() || old_size.unwrap() != new_size {
let mut child_ctx = LifeCycleCtx {
widget_state: &mut self.state,
state: ctx.state,
};
let size_event = LifeCycle::Size(new_size);
self.inner.lifecycle(&mut child_ctx, &size_event, data, env);
needs_merge = true;
}
if WidgetPod::set_hot_state(
&mut self.inner,
&mut self.state,
ctx.state,
layout_rect,
ctx.mouse_pos,
data,
env,
) {
needs_merge = true;
}
if needs_merge {
ctx.widget_state.merge_up(&mut self.state);
}
}
#[deprecated(since = "0.5.0", note = "use layout_rect() instead")]
#[doc(hidden)]
pub fn get_layout_rect(&self) -> Rect {
self.layout_rect()
}
pub fn layout_rect(&self) -> Rect {
self.state.layout_rect.unwrap_or_default()
}
pub fn set_viewport_offset(&mut self, offset: Vec2) {
self.state.viewport_offset = offset;
}
pub fn viewport_offset(&self) -> Vec2 {
self.state.viewport_offset
}
pub fn paint_rect(&self) -> Rect {
self.state.paint_rect()
}
pub fn paint_insets(&self) -> Insets {
self.state.paint_insets
}
pub fn compute_parent_paint_insets(&self, parent_size: Size) -> Insets {
let parent_bounds = Rect::ZERO.with_size(parent_size);
let union_pant_rect = self.paint_rect().union(parent_bounds);
union_pant_rect - parent_bounds
}
fn set_hot_state(
child: &mut W,
child_state: &mut WidgetState,
state: &mut ContextState,
rect: Rect,
mouse_pos: Option<Point>,
data: &T,
env: &Env,
) -> bool {
let had_hot = child_state.is_hot;
child_state.is_hot = match mouse_pos {
Some(pos) => rect.winding(pos) != 0,
None => false,
};
if had_hot != child_state.is_hot {
let hot_changed_event = LifeCycle::HotChanged(child_state.is_hot);
let mut child_ctx = LifeCycleCtx {
state,
widget_state: child_state,
};
child.lifecycle(&mut child_ctx, &hot_changed_event, data, env);
if env.get(Env::DEBUG_WIDGET_ID) {
child_ctx.request_paint();
}
return true;
}
false
}
}
impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
pub fn paint_raw(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
if env.get(Env::DEBUG_WIDGET_ID) {
self.make_widget_id_layout_if_needed(self.state.id, ctx, env);
}
let mut inner_ctx = PaintCtx {
render_ctx: ctx.render_ctx,
state: ctx.state,
z_ops: Vec::new(),
region: ctx.region.clone(),
widget_state: &self.state,
depth: ctx.depth,
};
self.inner.paint(&mut inner_ctx, data, env);
let debug_ids = inner_ctx.is_hot() && env.get(Env::DEBUG_WIDGET_ID);
if debug_ids {
self.debug_paint_widget_ids(&mut inner_ctx, env);
}
if !debug_ids && env.get(Env::DEBUG_PAINT) {
self.debug_paint_layout_bounds(&mut inner_ctx, env);
}
ctx.z_ops.append(&mut inner_ctx.z_ops);
self.state.invalid = Region::EMPTY;
}
pub fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
self.paint_impl(ctx, data, env, false)
}
pub fn paint_always(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
self.paint_impl(ctx, data, env, true)
}
fn paint_impl(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env, paint_if_not_visible: bool) {
if !paint_if_not_visible && !ctx.region().intersects(self.state.paint_rect()) {
return;
}
ctx.with_save(|ctx| {
let layout_origin = self.layout_rect().origin().to_vec2();
ctx.transform(Affine::translate(layout_origin));
let visible = ctx.region().to_rect().intersect(self.state.paint_rect()) - layout_origin;
ctx.with_child_ctx(visible, |ctx| self.paint_raw(ctx, data, env));
});
}
fn make_widget_id_layout_if_needed(&mut self, id: WidgetId, ctx: &mut PaintCtx, env: &Env) {
if self.debug_widget_text.is_none() {
let font = ctx
.text()
.new_font_by_name(env.get(crate::theme::FONT_NAME), 10.0)
.build()
.unwrap();
let id_string = id.to_raw().to_string();
self.debug_widget_text = ctx
.text()
.new_text_layout(&font, &id_string, f64::INFINITY)
.build()
.ok();
}
}
fn debug_paint_widget_ids(&self, ctx: &mut PaintCtx, env: &Env) {
let text = self.debug_widget_text.clone();
if let Some(text) = text {
let text_size = Size::new(text.width(), 10.0);
let origin = ctx.size().to_vec2() - text_size.to_vec2();
let border_color = env.get_debug_color(ctx.widget_id().to_raw());
self.debug_paint_layout_bounds(ctx, env);
ctx.paint_with_z_index(ctx.depth(), move |ctx| {
let origin = Point::new(origin.x.max(0.0), origin.y.max(0.0));
let text_pos = origin + Vec2::new(0., 8.0);
let text_rect = Rect::from_origin_size(origin, text_size);
ctx.fill(text_rect, &border_color);
let (r, g, b, _) = border_color.as_rgba8();
let avg = (r as u32 + g as u32 + b as u32) / 3;
let text_color = if avg < 128 {
Color::WHITE
} else {
Color::BLACK
};
ctx.draw_text(&text, text_pos, &text_color);
})
}
}
fn debug_paint_layout_bounds(&self, ctx: &mut PaintCtx, env: &Env) {
const BORDER_WIDTH: f64 = 1.0;
let rect = ctx.size().to_rect().inset(BORDER_WIDTH / -2.0);
let id = self.id().to_raw();
let color = env.get_debug_color(id);
ctx.stroke(rect, &color, BORDER_WIDTH);
}
pub fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &T,
env: &Env,
) -> Size {
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 {
widget_state: &mut self.state,
state: ctx.state,
mouse_pos: child_mouse_pos,
};
let size = self.inner.layout(&mut child_ctx, bc, data, env);
ctx.widget_state.merge_up(&mut child_ctx.widget_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);
}
size
}
pub fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
if self.old_data.is_none() {
log::error!(
"widget {:?} is receiving an event without having first \
received WidgetAdded.",
ctx.widget_id()
);
}
if self.state.layout_rect.is_none() {
match event {
Event::Internal(_) => (),
Event::Timer(_) => (),
Event::WindowConnected => (),
Event::WindowSize(_) => (),
_ => {
log::warn!(
"Widget '{}' received an event ({:?}) without having been laid out. \
This likely indicates a missed call to set_layout_rect.",
self.inner.type_name(),
event,
);
}
}
}
if ctx.is_handled {
return;
}
let had_active = self.state.has_active;
let rect = self.state.layout_rect.unwrap_or_default();
let mut modified_event = None;
let recurse = match event {
Event::Internal(internal) => match internal {
InternalEvent::MouseLeave => {
let hot_changed = WidgetPod::set_hot_state(
&mut self.inner,
&mut self.state,
ctx.state,
rect,
None,
data,
env,
);
had_active || hot_changed
}
InternalEvent::TargetedCommand(target, cmd) => {
match target {
Target::Widget(id) if *id == self.id() => {
modified_event = Some(Event::Command(cmd.clone()));
true
}
Target::Widget(id) => {
self.state.children.may_contain(id)
}
Target::Global | Target::Window(_) => {
modified_event = Some(Event::Command(cmd.clone()));
true
}
}
}
InternalEvent::RouteTimer(token, widget_id) => {
if *widget_id == self.id() {
modified_event = Some(Event::Timer(*token));
true
} else {
self.state.children.may_contain(widget_id)
}
}
},
Event::WindowConnected => true,
Event::WindowSize(_) => {
self.state.needs_layout = true;
ctx.is_root
}
Event::MouseDown(mouse_event) => {
WidgetPod::set_hot_state(
&mut self.inner,
&mut self.state,
ctx.state,
rect,
Some(mouse_event.pos),
data,
env,
);
if had_active || self.state.is_hot {
let mut mouse_event = mouse_event.clone();
mouse_event.pos -= rect.origin().to_vec2();
modified_event = Some(Event::MouseDown(mouse_event));
true
} else {
false
}
}
Event::MouseUp(mouse_event) => {
WidgetPod::set_hot_state(
&mut self.inner,
&mut self.state,
ctx.state,
rect,
Some(mouse_event.pos),
data,
env,
);
if had_active || self.state.is_hot {
let mut mouse_event = mouse_event.clone();
mouse_event.pos -= rect.origin().to_vec2();
modified_event = Some(Event::MouseUp(mouse_event));
true
} else {
false
}
}
Event::MouseMove(mouse_event) => {
let hot_changed = WidgetPod::set_hot_state(
&mut self.inner,
&mut self.state,
ctx.state,
rect,
Some(mouse_event.pos),
data,
env,
);
if had_active || self.state.is_hot || hot_changed {
let mut mouse_event = mouse_event.clone();
mouse_event.pos -= rect.origin().to_vec2();
modified_event = Some(Event::MouseMove(mouse_event));
true
} else {
false
}
}
Event::Wheel(mouse_event) => {
WidgetPod::set_hot_state(
&mut self.inner,
&mut self.state,
ctx.state,
rect,
Some(mouse_event.pos),
data,
env,
);
if had_active || self.state.is_hot {
let mut mouse_event = mouse_event.clone();
mouse_event.pos -= rect.origin().to_vec2();
modified_event = Some(Event::Wheel(mouse_event));
true
} else {
false
}
}
Event::KeyDown(_) => self.state.has_focus,
Event::KeyUp(_) => self.state.has_focus,
Event::Paste(_) => self.state.has_focus,
Event::Zoom(_) => had_active || self.state.is_hot,
Event::Timer(_) => false,
Event::Command(_) => true,
};
if recurse {
let mut inner_ctx = EventCtx {
cursor: ctx.cursor,
state: ctx.state,
widget_state: &mut self.state,
is_handled: false,
is_root: false,
};
let inner_event = modified_event.as_ref().unwrap_or(event);
inner_ctx.widget_state.has_active = false;
self.inner.event(&mut inner_ctx, &inner_event, data, env);
inner_ctx.widget_state.has_active |= inner_ctx.widget_state.is_active;
ctx.is_handled |= inner_ctx.is_handled;
}
ctx.widget_state.merge_up(&mut self.state);
}
pub fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
let mut extra_event = None;
let recurse = match event {
LifeCycle::Internal(internal) => match internal {
InternalLifeCycle::RouteWidgetAdded => {
if self.old_data.is_none() {
self.lifecycle(ctx, &LifeCycle::WidgetAdded, data, env);
return;
} else {
if self.state.children_changed {
self.state.children.clear();
self.state.focus_chain.clear();
}
self.state.children_changed
}
}
InternalLifeCycle::RouteFocusChanged { old, new } => {
let this_changed = if *old == Some(self.state.id) {
Some(false)
} else if *new == Some(self.state.id) {
Some(true)
} else {
None
};
if let Some(change) = this_changed {
self.state.has_focus = change;
extra_event = Some(LifeCycle::FocusChanged(change));
} else {
self.state.has_focus = false;
}
match (old, new) {
(Some(old), _) if self.state.children.may_contain(old) => true,
(_, Some(new)) if self.state.children.may_contain(new) => true,
_ => false,
}
}
#[cfg(test)]
InternalLifeCycle::DebugRequestState { widget, state_cell } => {
if *widget == self.id() {
state_cell.set(self.state.clone());
false
} else {
self.state.children.may_contain(&widget)
}
}
#[cfg(test)]
InternalLifeCycle::DebugInspectState(f) => {
f.call(&self.state);
true
}
},
LifeCycle::AnimFrame(_) => {
let r = self.state.request_anim;
self.state.request_anim = false;
r
}
LifeCycle::WidgetAdded => {
assert!(self.old_data.is_none());
self.old_data = Some(data.clone());
self.env = Some(env.clone());
true
}
LifeCycle::Size(_) => {
false
}
LifeCycle::HotChanged(_) => false,
LifeCycle::FocusChanged(_) => {
false
}
};
let mut child_ctx = LifeCycleCtx {
state: ctx.state,
widget_state: &mut self.state,
};
if recurse {
self.inner.lifecycle(&mut child_ctx, event, data, env);
}
if let Some(event) = extra_event.as_ref() {
self.inner.lifecycle(&mut child_ctx, event, data, env);
}
ctx.widget_state.merge_up(&mut self.state);
match event {
LifeCycle::WidgetAdded | LifeCycle::Internal(InternalLifeCycle::RouteWidgetAdded) => {
self.state.children_changed = false;
ctx.widget_state.children = ctx.widget_state.children.union(self.state.children);
ctx.widget_state.focus_chain.extend(&self.state.focus_chain);
ctx.register_child(self.id());
}
_ => (),
}
}
pub fn update(&mut self, ctx: &mut UpdateCtx, data: &T, env: &Env) {
match (self.old_data.as_ref(), self.env.as_ref()) {
(Some(d), Some(e)) if d.same(data) && e.same(env) => return,
(None, _) => {
log::warn!("old_data missing in {:?}, skipping update", self.id());
self.old_data = Some(data.clone());
self.env = Some(env.clone());
return;
}
_ => (),
}
let mut child_ctx = UpdateCtx {
state: ctx.state,
widget_state: &mut self.state,
};
self.inner
.update(&mut child_ctx, self.old_data.as_ref().unwrap(), data, env);
self.old_data = Some(data.clone());
self.env = Some(env.clone());
ctx.widget_state.merge_up(&mut self.state)
}
}
impl<T, W: Widget<T> + 'static> WidgetPod<T, W> {
pub fn boxed(self) -> WidgetPod<T, Box<dyn Widget<T>>> {
WidgetPod::new(Box::new(self.inner))
}
}
impl WidgetState {
pub(crate) fn new(id: WidgetId) -> WidgetState {
WidgetState {
id,
layout_rect: None,
paint_insets: Insets::ZERO,
invalid: Region::EMPTY,
viewport_offset: Vec2::ZERO,
is_hot: false,
needs_layout: false,
is_active: false,
has_active: false,
has_focus: false,
request_anim: false,
request_focus: None,
focus_chain: Vec::new(),
children: Bloom::new(),
children_changed: false,
timers: HashMap::new(),
}
}
pub(crate) fn add_timer(&mut self, timer_token: TimerToken) {
self.timers.insert(timer_token, self.id);
}
fn merge_up(&mut self, child_state: &mut WidgetState) {
let mut child_region = child_state.invalid.clone();
child_region += child_state.layout_rect().origin().to_vec2() - child_state.viewport_offset;
let clip = self
.layout_rect()
.with_origin(Point::ORIGIN)
.inset(self.paint_insets);
child_region.intersect_with(clip);
self.invalid.merge_with(child_region);
self.needs_layout |= child_state.needs_layout;
self.request_anim |= child_state.request_anim;
self.has_active |= child_state.has_active;
self.has_focus |= child_state.has_focus;
self.children_changed |= child_state.children_changed;
self.request_focus = child_state.request_focus.take().or(self.request_focus);
self.timers.extend_drain(&mut child_state.timers);
}
#[inline]
pub(crate) fn size(&self) -> Size {
self.layout_rect.unwrap_or_default().size()
}
pub(crate) fn paint_rect(&self) -> Rect {
self.layout_rect.unwrap_or_default() + self.paint_insets
}
pub(crate) fn layout_rect(&self) -> Rect {
self.layout_rect.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::{Flex, Scroll, Split, TextBox};
use crate::{WidgetExt, WindowHandle, WindowId};
const ID_1: WidgetId = WidgetId::reserved(0);
const ID_2: WidgetId = WidgetId::reserved(1);
const ID_3: WidgetId = WidgetId::reserved(2);
#[test]
fn register_children() {
fn make_widgets() -> impl Widget<Option<u32>> {
Split::columns(
Flex::<Option<u32>>::row()
.with_child(TextBox::new().with_id(ID_1).parse())
.with_child(TextBox::new().with_id(ID_2).parse())
.with_child(TextBox::new().with_id(ID_3).parse()),
Scroll::new(TextBox::new().parse()),
)
}
let widget = make_widgets();
let mut widget = WidgetPod::new(widget).boxed();
let mut command_queue: CommandQueue = VecDeque::new();
let mut widget_state = WidgetState::new(WidgetId::next());
let mut state = ContextState {
command_queue: &mut command_queue,
window_id: WindowId::next(),
window: &WindowHandle::default(),
root_app_data_type: std::any::TypeId::of::<Option<u32>>(),
focus_widget: None,
};
let mut ctx = LifeCycleCtx {
widget_state: &mut widget_state,
state: &mut state,
};
let env = Env::default();
widget.lifecycle(&mut ctx, &LifeCycle::WidgetAdded, &None, &env);
assert!(ctx.widget_state.children.may_contain(&ID_1));
assert!(ctx.widget_state.children.may_contain(&ID_2));
assert!(ctx.widget_state.children.may_contain(&ID_3));
assert_eq!(ctx.widget_state.children.entry_count(), 7);
}
}