xilem/masonry/src/render_root.rs

743 lines
26 KiB
Rust

// Copyright 2019 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
use std::collections::{HashMap, VecDeque};
use accesskit::{ActionRequest, TreeUpdate};
use parley::fontique::{self, Collection, CollectionOptions};
use parley::{FontContext, LayoutContext};
use tracing::{info_span, warn};
use tree_arena::{ArenaMut, TreeArena};
use vello::kurbo::{self, Rect};
use vello::Scene;
use winit::window::ResizeDirection;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
use crate::event::{PointerEvent, TextEvent, WindowEvent};
use crate::passes::accessibility::run_accessibility_pass;
use crate::passes::anim::run_update_anim_pass;
use crate::passes::compose::run_compose_pass;
use crate::passes::event::{
run_on_access_event_pass, run_on_pointer_event_pass, run_on_text_event_pass,
};
use crate::passes::layout::run_layout_pass;
use crate::passes::mutate::{mutate_widget, run_mutate_pass};
use crate::passes::paint::run_paint_pass;
use crate::passes::update::{
run_update_disabled_pass, run_update_focus_chain_pass, run_update_focus_pass,
run_update_pointer_pass, run_update_scroll_pass, run_update_stashed_pass,
run_update_widget_tree_pass,
};
use crate::passes::{recurse_on_children, PassTracing};
use crate::text::BrushIndex;
use crate::widget::{WidgetArena, WidgetMut, WidgetRef, WidgetState};
use crate::{AccessEvent, Action, CursorIcon, Handled, QueryCtx, Widget, WidgetId, WidgetPod};
/// We ensure that any valid initial IME area is sent to the platform by storing an invalid initial
/// IME area as the `last_sent_ime_area`.
const INVALID_IME_AREA: Rect = Rect::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN);
// --- MARK: STRUCTS ---
/// The composition root of Masonry.
///
/// This is the entry point for all user events, and the source of all signals to be sent to
/// winit or similar event loop runners, as well as 2D scenes and accessibility information.
///
/// This is also the type that owns the widget tree.
pub struct RenderRoot {
/// Root of the widget tree.
pub(crate) root: WidgetPod<dyn Widget>,
/// Whether the window size should be determined by the content or the user.
pub(crate) size_policy: WindowSizePolicy,
/// Current size of the window.
pub(crate) size: PhysicalSize<u32>,
/// DPI scale factor.
///
/// Kurbo coordinates are assumed to be in logical pixels
pub(crate) scale_factor: f64,
/// Is `Some` if the most recently displayed frame was an animation frame.
pub(crate) last_anim: Option<Instant>,
/// Last mouse position. Updated by `on_pointer_event` pass, used by other passes.
pub(crate) last_mouse_pos: Option<LogicalPosition<f64>>,
/// State passed to context types.
pub(crate) global_state: RenderRootState,
/// Whether the next accessibility pass should rebuild the entire access tree.
///
/// TODO - Add `access_tree_active` to detect when you don't need to update the
// access tree
pub(crate) rebuild_access_tree: bool,
/// The widget tree; stores widgets and their states.
pub(crate) widget_arena: WidgetArena,
pub(crate) debug_paint: bool,
}
/// State shared between passes.
pub(crate) struct RenderRootState {
/// Queue of signals to be processed by the event loop.
pub(crate) signal_queue: VecDeque<RenderRootSignal>,
/// Currently focused widget.
pub(crate) focused_widget: Option<WidgetId>,
/// List of ancestors of the currently focused widget.
pub(crate) focused_path: Vec<WidgetId>,
/// Widget that will be focused once the `update_focus` pass is run.
pub(crate) next_focused_widget: Option<WidgetId>,
/// Most recently clicked widget.
///
/// This is used to pick the focused widget on Tab events.
pub(crate) most_recently_clicked_widget: Option<WidgetId>,
/// Whether the window is focused.
pub(crate) window_focused: bool,
/// Widgets that have requested to be scrolled into view.
pub(crate) scroll_request_targets: Vec<(WidgetId, Rect)>,
/// List of ancestors of the currently hovered widget.
pub(crate) hovered_path: Vec<WidgetId>,
/// Widget that currently has pointer capture.
pub(crate) pointer_capture_target: Option<WidgetId>,
/// Current cursor icon.
pub(crate) cursor_icon: CursorIcon,
/// Cache for Parley font data.
pub(crate) font_context: FontContext,
/// Cache for Parley text layout data.
pub(crate) text_layout_context: LayoutContext<BrushIndex>,
/// List of callbacks that will run in the next `mutate` pass.
pub(crate) mutate_callbacks: Vec<MutateCallback>,
/// Whether an IME session is active.
pub(crate) is_ime_active: bool,
/// The cursor area last sent to the platform.
pub(crate) last_sent_ime_area: Rect,
/// Scene cache for the widget tree.
pub(crate) scenes: HashMap<WidgetId, Scene>,
/// Whether data set in the pointer pass has been invalidated.
pub(crate) needs_pointer_pass: bool,
/// Pass tracing configuration, used to skip tracing to limit overhead.
pub(crate) trace: PassTracing,
pub(crate) inspector_state: InspectorState,
}
pub(crate) struct MutateCallback {
pub(crate) id: WidgetId,
pub(crate) callback: Box<dyn FnOnce(WidgetMut<'_, dyn Widget>)>,
}
/// Defines how a windows size should be determined
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum WindowSizePolicy {
/// Use the content of the window to determine the size.
///
/// If you use this option, your root widget will be passed infinite constraints;
/// you are responsible for ensuring that your content picks an appropriate size.
Content,
/// Use the provided window size.
#[default]
User,
}
/// Options for creating a [`RenderRoot`].
pub struct RenderRootOptions {
/// If true, `fontique` will provide access to system fonts
/// using platform-specific APIs.
pub use_system_fonts: bool,
/// Defines how the window size should be determined.
pub size_policy: WindowSizePolicy,
/// The scale factor to use for rendering.
///
/// Useful for high-DPI displays.
///
/// `1.0` is a sensible default.
pub scale_factor: f64,
/// Add a font from its raw data for use in tests.
/// The font is added to the fallback chain for Latin scripts.
/// This is expected to be used with `use_system_fonts = false`
/// to ensure rendering is consistent cross-platform.
///
/// We expect to develop a much more fully-featured font API in the future, but
/// this is necessary for our testing of Masonry.
pub test_font: Option<Vec<u8>>,
}
/// Objects emitted by the [`RenderRoot`] to signal that something has changed or require external actions.
pub enum RenderRootSignal {
/// A widget has emitted an action.
Action(Action, WidgetId),
/// An IME session has been started.
StartIme,
/// The IME session has ended.
EndIme,
/// The IME area has been moved.
ImeMoved(LogicalPosition<f64>, LogicalSize<f64>),
/// The window needs to be redrawn.
RequestRedraw,
/// The window should be redrawn for an animation frame. Currently this isn't really different from `RequestRedraw`.
RequestAnimFrame,
/// The window should take focus.
TakeFocus,
/// The mouse icon has changed.
SetCursor(CursorIcon),
/// The window size has changed.
SetSize(PhysicalSize<u32>),
/// The window title has changed.
SetTitle(String),
/// The window is being dragged.
DragWindow,
/// The window is being resized.
DragResizeWindow(ResizeDirection),
/// The window is being maximized.
ToggleMaximized,
/// The window is being minimized.
Minimize,
/// The window is being closed.
Exit,
/// The window menu is being shown.
ShowWindowMenu(LogicalPosition<f64>),
/// The widget picker has selected this widget.
WidgetSelectedInInspector(WidgetId),
}
/// State of the widget inspector. Useful for debugging.
///
/// Widget inspector is WIP. It should get its own standalone documentation.
pub(crate) struct InspectorState {
pub(crate) is_picking_widget: bool,
pub(crate) hovered_widget: Option<WidgetId>,
}
impl RenderRoot {
/// Create a new `RenderRoot` with the given options.
///
/// Note that this doesn't create a window or start the event loop.
///
/// See [`crate::event_loop_runner::run`] for that.
pub fn new(root_widget: impl Widget, options: RenderRootOptions) -> Self {
let RenderRootOptions {
use_system_fonts,
size_policy,
scale_factor,
test_font,
} = options;
let debug_paint = std::env::var("MASONRY_DEBUG_PAINT").is_ok_and(|it| !it.is_empty());
let mut root = Self {
root: WidgetPod::new(root_widget).erased(),
size_policy,
size: PhysicalSize::new(0, 0),
scale_factor,
last_anim: None,
last_mouse_pos: None,
global_state: RenderRootState {
signal_queue: VecDeque::new(),
focused_widget: None,
focused_path: Vec::new(),
next_focused_widget: None,
most_recently_clicked_widget: None,
window_focused: true,
scroll_request_targets: Vec::new(),
hovered_path: Vec::new(),
pointer_capture_target: None,
cursor_icon: CursorIcon::Default,
font_context: FontContext {
collection: Collection::new(CollectionOptions {
system_fonts: use_system_fonts,
..Default::default()
}),
source_cache: Default::default(),
},
text_layout_context: LayoutContext::new(),
mutate_callbacks: Vec::new(),
is_ime_active: false,
last_sent_ime_area: INVALID_IME_AREA,
scenes: HashMap::new(),
needs_pointer_pass: false,
trace: PassTracing::from_env(),
inspector_state: InspectorState {
is_picking_widget: false,
hovered_widget: None,
},
},
widget_arena: WidgetArena {
widgets: TreeArena::new(),
states: TreeArena::new(),
},
rebuild_access_tree: true,
debug_paint,
};
if let Some(test_font_data) = test_font {
let families = root.register_fonts(test_font_data);
// Make sure that all of these fonts are in the fallback chain for the Latin script.
// <https://en.wikipedia.org/wiki/Script_(Unicode)#Latn>
root.global_state
.font_context
.collection
.append_fallbacks(*b"Latn", families.iter().map(|(family, _)| *family));
}
// We run a set of passes to initialize the widget tree
root.run_rewrite_passes();
root
}
pub(crate) fn root_state(&self) -> &WidgetState {
self.widget_arena
.states
.root_token()
.into_child(self.root.id())
.expect("root widget not in widget tree")
.item
}
pub(crate) fn root_state_mut(&mut self) -> &mut WidgetState {
self.widget_arena
.states
.root_token_mut()
.into_child_mut(self.root.id())
.expect("root widget not in widget tree")
.item
}
// --- MARK: WINDOW_EVENT ---
/// Handle a window event.
pub fn handle_window_event(&mut self, event: WindowEvent) -> Handled {
match event {
WindowEvent::Rescale(scale_factor) => {
self.scale_factor = scale_factor;
self.request_render_all();
Handled::Yes
}
WindowEvent::Resize(size) => {
self.size = size;
self.root_state_mut().request_layout = true;
self.root_state_mut().needs_layout = true;
self.run_rewrite_passes();
Handled::Yes
}
WindowEvent::AnimFrame => {
let now = Instant::now();
// TODO: this calculation uses wall-clock time of the paint call, which
// potentially has jitter.
//
// See https://github.com/linebender/druid/issues/85 for discussion.
let last = self.last_anim.take();
let elapsed_ns = last.map(|t| now.duration_since(t).as_nanos()).unwrap_or(0) as u64;
run_update_anim_pass(self, elapsed_ns);
self.run_rewrite_passes();
// If this animation will continue, store the time.
// If a new animation starts, then it will have zero reported elapsed time.
let animation_continues = self.root_state().needs_anim;
self.last_anim = animation_continues.then_some(now);
Handled::Yes
}
WindowEvent::RebuildAccessTree => {
self.rebuild_access_tree = true;
self.global_state
.emit_signal(RenderRootSignal::RequestRedraw);
Handled::Yes
}
}
}
// --- MARK: PUB FUNCTIONS ---
/// Handle a pointer event.
pub fn handle_pointer_event(&mut self, event: PointerEvent) -> Handled {
let _span = info_span!("pointer_event");
let handled = run_on_pointer_event_pass(self, &event);
run_update_pointer_pass(self);
self.run_rewrite_passes();
handled
}
/// Handle a text event.
pub fn handle_text_event(&mut self, event: TextEvent) -> Handled {
let _span = info_span!("text_event");
let handled = run_on_text_event_pass(self, &event);
run_update_focus_pass(self);
if matches!(event, TextEvent::Ime(winit::event::Ime::Enabled)) {
// Reset the last sent IME area, as the platform reset the IME state and may have
// forgotten it.
self.global_state.last_sent_ime_area = INVALID_IME_AREA;
}
self.run_rewrite_passes();
handled
}
/// Handle an accesskit event.
pub fn handle_access_event(&mut self, event: ActionRequest) {
let _span = info_span!("access_event");
let Ok(id) = event.target.0.try_into() else {
warn!("Received ActionRequest with id 0. This shouldn't be possible.");
return;
};
let event = AccessEvent {
action: event.action,
data: event.data,
};
run_on_access_event_pass(self, &event, WidgetId(id));
self.run_rewrite_passes();
}
/// Registers all fonts that exist in the given data.
///
/// Returns a list of pairs each containing the family identifier and fonts
/// added to that family.
pub fn register_fonts(
&mut self,
data: Vec<u8>,
) -> Vec<(fontique::FamilyId, Vec<fontique::FontInfo>)> {
self.global_state
.font_context
.collection
.register_fonts(data)
}
/// Redraw the window.
///
/// Returns an update to the accessibility tree and a Vello scene representing
/// the widget tree's current state.
pub fn redraw(&mut self) -> (Scene, TreeUpdate) {
if self.root_state().needs_layout {
// TODO - Rewrite more clearly after run_rewrite_passes is rewritten
self.run_rewrite_passes();
}
if self.root_state().needs_layout {
warn!("Widget requested layout during layout pass");
self.global_state
.emit_signal(RenderRootSignal::RequestRedraw);
}
// TODO - Handle invalidation regions
(
run_paint_pass(self),
run_accessibility_pass(self, self.scale_factor),
)
}
/// Pop the oldest signal from the queue.
pub fn pop_signal(&mut self) -> Option<RenderRootSignal> {
self.global_state.signal_queue.pop_front()
}
/// Pop the oldest signal from the queue that matches the predicate.
///
/// Doesn't affect other signals.
///
/// Note that you should still use [`Self::pop_signal`] to avoid letting the queue
/// grow indefinitely.
pub fn pop_signal_matching(
&mut self,
predicate: impl Fn(&RenderRootSignal) -> bool,
) -> Option<RenderRootSignal> {
let idx = self.global_state.signal_queue.iter().position(predicate)?;
self.global_state.signal_queue.remove(idx)
}
/// Get the current icon that the mouse should display.
pub fn cursor_icon(&self) -> CursorIcon {
self.global_state.cursor_icon
}
// --- MARK: ACCESS WIDGETS---
/// Get a [`WidgetRef`] to the root widget.
pub fn get_root_widget(&self) -> WidgetRef<dyn Widget> {
let root_state_token = self.widget_arena.states.root_token();
let root_widget_token = self.widget_arena.widgets.root_token();
let state_ref = root_state_token
.into_child(self.root.id())
.expect("root widget not in widget tree");
let widget_ref = root_widget_token
.into_child(self.root.id())
.expect("root widget not in widget tree");
let widget = &**widget_ref.item;
let ctx = QueryCtx {
global_state: &self.global_state,
widget_state_children: state_ref.children,
widget_children: widget_ref.children,
widget_state: state_ref.item,
};
WidgetRef { ctx, widget }
}
/// Get a [`WidgetRef`] to a specific widget.
pub fn get_widget(&self, id: WidgetId) -> Option<WidgetRef<dyn Widget>> {
let state_ref = self.widget_arena.states.find(id)?;
let widget_ref = self
.widget_arena
.widgets
.find(id)
.expect("found state but not widget");
let widget = &**widget_ref.item;
let ctx = QueryCtx {
global_state: &self.global_state,
widget_state_children: state_ref.children,
widget_children: widget_ref.children,
widget_state: state_ref.item,
};
Some(WidgetRef { ctx, widget })
}
/// Get a [`WidgetMut`] to the root widget.
///
/// Because of how `WidgetMut` works, it can only be passed to a user-provided callback.
pub fn edit_root_widget<R>(&mut self, f: impl FnOnce(WidgetMut<'_, dyn Widget>) -> R) -> R {
let res = mutate_widget(self, self.root.id(), f);
self.run_rewrite_passes();
res
}
/// Get a [`WidgetMut`] to a specific widget.
///
/// Because of how `WidgetMut` works, it can only be passed to a user-provided callback.
pub fn edit_widget<R>(
&mut self,
id: WidgetId,
f: impl FnOnce(WidgetMut<'_, dyn Widget>) -> R,
) -> R {
let res = mutate_widget(self, id, f);
self.run_rewrite_passes();
res
}
pub(crate) fn get_kurbo_size(&self) -> kurbo::Size {
let size = self.size.to_logical(self.scale_factor);
kurbo::Size::new(size.width, size.height)
}
// --- MARK: REWRITE PASSES ---
/// Run all rewrite passes on widget tree.
///
/// Rewrite passes are passes which occur after external events, and
/// update flags and internal values to a consistent state.
///
/// See Pass Spec RFC for details. (TODO - Link to doc instead.)
pub(crate) fn run_rewrite_passes(&mut self) {
const REWRITE_PASSES_MAX: usize = 4;
for _ in 0..REWRITE_PASSES_MAX {
// Note: this code doesn't do any short-circuiting, because each pass is
// expected to have its own early exits.
// Calling a run_xxx_pass (or root_xxx) should always be very fast if
// the pass doesn't need to do anything.
run_mutate_pass(self);
run_update_widget_tree_pass(self);
run_update_disabled_pass(self);
run_update_stashed_pass(self);
run_update_focus_chain_pass(self);
run_update_focus_pass(self);
run_layout_pass(self);
run_update_scroll_pass(self);
run_compose_pass(self);
run_update_pointer_pass(self);
if !self.root_state().needs_rewrite_passes()
&& !self.global_state.needs_rewrite_passes()
{
break;
}
}
if self.root_state().needs_rewrite_passes() || self.global_state.needs_rewrite_passes() {
warn!("All rewrite passes have run {REWRITE_PASSES_MAX} times, but invalidations are still set");
// To avoid an infinite loop, we delay re-running the passes until the next frame.
self.global_state
.emit_signal(RenderRootSignal::RequestRedraw);
}
if self.root_state().needs_anim {
self.global_state
.emit_signal(RenderRootSignal::RequestAnimFrame);
}
// We request a redraw if either the render tree or the accessibility
// tree needs to be rebuilt. Usually both happen at the same time.
// A redraw will trigger a rebuild of the accessibility tree.
// TODO - We assume that a relayout will trigger a repaint
if self.root_state().needs_paint || self.root_state().needs_accessibility {
self.global_state
.emit_signal(RenderRootSignal::RequestRedraw);
}
if self.global_state.is_ime_active {
let widget = self
.global_state
.focused_widget
.expect("IME is active without a focused widget");
let ime_area = self.widget_arena.get_state(widget).item.get_ime_area();
// Certain desktop environments (primarily KDE on Wayland) re-synchronise IME state
// with the client (this app) in response to the safe area changing.
// Our handling of that ultimately results in us sending the safe area again,
// which causes an infinite loop.
// We break that loop by not re-sending the same safe area again.
if self.global_state.last_sent_ime_area != ime_area {
self.global_state.last_sent_ime_area = ime_area;
self.global_state
.emit_signal(RenderRootSignal::new_ime_moved_signal(ime_area));
}
}
}
pub(crate) fn request_render_all(&mut self) {
fn request_render_all_in(
mut widget: ArenaMut<'_, Box<dyn Widget>>,
state: ArenaMut<'_, WidgetState>,
) {
state.item.needs_paint = true;
state.item.needs_accessibility = true;
state.item.request_paint = true;
state.item.request_accessibility = true;
let id = state.item.id;
recurse_on_children(
id,
widget.reborrow_mut(),
state.children,
|widget, mut state| {
request_render_all_in(widget, state.reborrow_mut());
},
);
}
let (root_widget, mut root_state) = self.widget_arena.get_pair_mut(self.root.id());
request_render_all_in(root_widget, root_state.reborrow_mut());
self.global_state
.emit_signal(RenderRootSignal::RequestRedraw);
}
/// Checks whether the given id points to a widget that is "interactive".
/// i.e. not disabled or stashed.
/// Only interactive widgets can have text focus or pointer capture.
pub(crate) fn is_still_interactive(&self, id: WidgetId) -> bool {
let Some(state) = self.widget_arena.states.find(id) else {
return false;
};
!state.item.is_stashed && !state.item.is_disabled
}
pub(crate) fn widget_from_focus_chain(&mut self, forward: bool) -> Option<WidgetId> {
let focused_widget = self
.global_state
.focused_widget
.or(self.global_state.most_recently_clicked_widget);
let focused_idx = focused_widget.and_then(|focused_widget| {
self.focus_chain()
.iter()
// Find where the focused widget is in the focus chain
.position(|id| id == &focused_widget)
});
if let Some(idx) = focused_idx {
// Return the id that's next to it in the focus chain
let len = self.focus_chain().len();
let new_idx = if forward {
(idx + 1) % len
} else {
(idx + len - 1) % len
};
Some(self.focus_chain()[new_idx])
} else {
// If no widget is currently focused or the
// currently focused widget isn't in the focus chain,
// then we'll just return the first/last entry of the chain, if any.
if forward {
self.focus_chain().first().copied()
} else {
self.focus_chain().last().copied()
}
}
}
// TODO - Store in RenderRootState
pub(crate) fn focus_chain(&mut self) -> &[WidgetId] {
&self.root_state().focus_chain
}
pub(crate) fn needs_rewrite_passes(&self) -> bool {
self.root_state().needs_rewrite_passes() || self.global_state.focus_changed()
}
}
impl RenderRootState {
/// Send a signal to the runner of this app, which allows global actions to be triggered by a widget.
pub(crate) fn emit_signal(&mut self, signal: RenderRootSignal) {
self.signal_queue.push_back(signal);
}
pub(crate) fn focus_changed(&self) -> bool {
self.focused_widget != self.next_focused_widget
}
#[expect(
dead_code,
reason = "no longer used, but may be useful again in the future"
)]
pub(crate) fn is_focused(&self, id: WidgetId) -> bool {
self.focused_widget == Some(id)
}
pub(crate) fn needs_rewrite_passes(&self) -> bool {
self.needs_pointer_pass || self.focused_widget != self.next_focused_widget
}
}
impl RenderRootSignal {
pub(crate) fn new_ime_moved_signal(area: Rect) -> Self {
Self::ImeMoved(
LogicalPosition {
x: area.origin().x,
y: area.origin().y,
},
LogicalSize {
width: area.size().width,
height: area.size().height,
},
)
}
}