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

Implement paint and accessibility passes #522

Merged
merged 21 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
98 changes: 59 additions & 39 deletions masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,7 @@ pub struct PaintCtx<'a> {
pub(crate) widget_state: &'a WidgetState,
pub(crate) widget_state_children: ArenaMutChildren<'a, WidgetState>,
pub(crate) widget_children: ArenaMutChildren<'a, Box<dyn Widget>>,
/// The approximate depth in the tree at the time of painting.
pub(crate) depth: u32,
pub(crate) debug_paint: bool,
pub(crate) debug_widget: bool,
}

pub struct AccessCtx<'a> {
Expand Down Expand Up @@ -171,6 +168,7 @@ impl_context_method!(
EventCtx<'_>,
LifeCycleCtx<'_>,
LayoutCtx<'_>,
ComposeCtx<'_>,
{
/// Helper method to get a mutable reference to a child widget's `WidgetState` from its `WidgetPod`.
///
Expand Down Expand Up @@ -405,6 +403,7 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {
/// Request a [`paint`](crate::Widget::paint) pass.
pub fn request_paint(&mut self) {
trace!("request_paint");
self.widget_state.request_paint = true;
self.widget_state.needs_paint = true;
}

Expand All @@ -420,6 +419,7 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {
/// [`layout`]: crate::Widget::layout
pub fn request_layout(&mut self) {
trace!("request_layout");
self.widget_state.request_layout = true;
self.widget_state.needs_layout = true;
}

Expand All @@ -436,8 +436,8 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {

pub fn request_accessibility_update(&mut self) {
trace!("request_accessibility_update");
self.widget_state.needs_accessibility_update = true;
self.widget_state.request_accessibility_update = true;
self.widget_state.needs_accessibility = true;
self.widget_state.request_accessibility = true;
}

/// Request an animation frame.
Expand Down Expand Up @@ -471,6 +471,7 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {
.widget_children
.remove_child(id)
.expect("remove_child: child not found");
self.global_state.scenes.remove(&child.id());

self.children_changed();
}
Expand All @@ -490,14 +491,6 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {
self.widget_state.is_explicitly_disabled_new = disabled;
}

/// Mark child widget as stashed.
///
/// **Note:** Stashed widgets are a WIP feature
pub fn set_stashed(&mut self, child: &mut WidgetPod<impl Widget>, stashed: bool) {
self.get_child_state_mut(child).is_stashed = stashed;
self.children_changed();
}

#[allow(unused)]
/// Indicate that text input state has changed.
///
Expand Down Expand Up @@ -565,6 +558,22 @@ impl_context_method!(
pub fn request_timer(&mut self, _deadline: Duration) -> TimerToken {
todo!("request_timer");
}

/// Mark child widget as stashed.
///
/// If `stashed` is true, the child will not be painted or listed in the accessibility tree.
///
/// This will *not* trigger a layout pass.
///
/// **Note:** Stashed widgets are a WIP feature
pub fn set_stashed(&mut self, child: &mut WidgetPod<impl Widget>, stashed: bool) {
if self.get_child_state_mut(child).is_stashed != stashed {
self.widget_state.children_changed = true;
self.widget_state.update_focus_chain = true;
}

self.get_child_state_mut(child).is_stashed = stashed;
}
}
);

Expand Down Expand Up @@ -868,6 +877,34 @@ impl LayoutCtx<'_> {
self.get_child_state_mut(child).needs_layout = false;
}

/// Gives the widget a clip path.
///
/// A widget's clip path will have two effects:
/// - It serves as a mask for painting operations of the widget's children (*not* the widget itself).
/// - Pointer events must be inside that path to reach the widget's children.
pub fn set_clip_path(&mut self, path: Rect) {
trace!("set_clip_path {:?}", path);
self.widget_state.clip = Some(path);
// TODO - Updating the clip path may have
// other knock-on effects we'd need to document.
self.widget_state.request_accessibility = true;
self.widget_state.needs_accessibility = true;
self.widget_state.needs_paint = true;
Comment on lines +890 to +892
Copy link
Contributor

Choose a reason for hiding this comment

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

I've seen this repeated at least three times. It may make sense to create a function just for marking the state change for needing paint and accessibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd like to keep these kinds of refactor for last, especially if we're only factoring three lines or less. I think there's a benefit to being able to see directly what the code does instead of jumping to another function.

}

/// Remove the widget's clip path.
///
/// See [`LayoutCtx::set_clip_path`] for details.
pub fn clear_clip_path(&mut self) {
trace!("clear_clip_path");
self.widget_state.clip = None;
// TODO - Updating the clip path may have
// other knock-on effects we'd need to document.
self.widget_state.request_accessibility = true;
self.widget_state.needs_accessibility = true;
self.widget_state.needs_paint = true;
}

/// Set the position of a child widget, in the parent's coordinate space. This
/// will also implicitly change "hot" status and affect the parent's display rect.
///
Expand Down Expand Up @@ -903,10 +940,15 @@ impl ComposeCtx<'_> {
/// Set a translation for the child widget.
///
/// The translation is applied on top of the position from [`LayoutCtx::place_child`].
pub fn set_child_translation(&mut self, translation: Vec2) {
if self.widget_state.translation != translation {
self.widget_state.translation = translation;
self.widget_state.translation_changed = true;
pub fn set_child_translation<W: Widget>(
&mut self,
child: &mut WidgetPod<W>,
translation: Vec2,
) {
let child = self.get_child_state_mut(child);
if translation != child.translation {
child.translation = translation;
child.translation_changed = true;
}
}
}
Expand All @@ -923,18 +965,6 @@ impl_context_method!(LayoutCtx<'_>, PaintCtx<'_>, {
});

impl PaintCtx<'_> {
/// The depth in the tree of the currently painting widget.
///
/// This may be used in combination with [`paint_with_z_index`](Self::paint_with_z_index) in order
/// to correctly order painting operations.
///
/// The `depth` here may not be exact; it is only guaranteed that a child will
/// have a greater depth than its parent.
#[inline]
pub fn depth(&self) -> u32 {
self.depth
}

// signal may be useful elsewhere, but is currently only used on PaintCtx
/// Submit a [`RenderRootSignal`]
///
Expand All @@ -948,16 +978,6 @@ impl AccessCtx<'_> {
pub fn current_node(&mut self) -> &mut NodeBuilder {
&mut self.current_node
}

/// Report whether accessibility was requested on this widget.
///
/// This method is primarily intended for containers. The `accessibility`
/// method will be called on a widget when it or any of its descendants
/// have seen a request. However, in many cases a container need not push
/// a node for itself.
pub fn is_requested(&self) -> bool {
self.widget_state.needs_accessibility_update
}
}

// --- MARK: RAW WRAPPERS ---
Expand Down
187 changes: 187 additions & 0 deletions masonry/src/passes/accessibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

use crate::render_root::RenderRoot;
use crate::tree_arena::ArenaMutChildren;
use accesskit::{NodeBuilder, NodeId, TreeUpdate};
use tracing::debug;
use tracing::info_span;
use tracing::trace;
use vello::kurbo::Rect;

use crate::passes::recurse_on_children;
use crate::render_root::RenderRootState;
use crate::tree_arena::ArenaMut;
use crate::{AccessCtx, Widget, WidgetState};

fn build_accessibility_tree(
global_state: &mut RenderRootState,
tree_update: &mut TreeUpdate,
mut widget: ArenaMut<'_, Box<dyn Widget>>,
mut state: ArenaMut<'_, WidgetState>,
rebuild_all: bool,
scale_factor: f64,
) {
let _span = widget.item.make_trace_span().entered();
let id = state.item.id;

if !rebuild_all && !state.item.needs_accessibility {
return;
}

if rebuild_all || state.item.request_accessibility {
trace!(
"Building accessibility node for widget '{}' #{}",
widget.item.short_type_name(),
id.to_raw(),
);
let current_node = build_access_node(
widget.item,
state.item,
state.children.reborrow_mut(),
scale_factor,
);

let mut ctx = AccessCtx {
global_state,
widget_state: state.item,
widget_state_children: state.children.reborrow_mut(),
widget_children: widget.children.reborrow_mut(),
tree_update,
current_node,
rebuild_all,
scale_factor,
};
widget.item.accessibility(&mut ctx);

let id: NodeId = ctx.widget_state.id.into();
trace!(
"Built node #{} with role={:?}, default_action={:?}",
id.0,
ctx.current_node.role(),
ctx.current_node.default_action_verb(),
);
ctx.tree_update.nodes.push((id, ctx.current_node.build()));
}

state.item.request_accessibility = false;
state.item.needs_accessibility = false;
Comment on lines +67 to +68
Copy link
Member

Choose a reason for hiding this comment

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

I suppose that since AccessCtx doesn't have these methods, it doesn't matter when these values are set, so long as it's after they are read?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assume that by "these methods" you mean "the methods that gave me problems in the layout PR"?

To be honest, I haven't decided on (or documented) a principled solution for that class of problem. I'm mostly deferring it until all passes are implemented.

But yeah, your reasoning makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

In this case, I meant request_accessibility. I hadn't checked if get_raw_mut were available in AccessCtx


let id = state.item.id;
let parent_state = state.item;
recurse_on_children(
id,
widget.reborrow_mut(),
state.children,
|widget, mut state| {
// TODO - We skip updating stashed items.
// This may have knock-on effects we'd need to document.
if state.item.is_stashed {
return;
}
build_accessibility_tree(
global_state,
tree_update,
widget,
state.reborrow_mut(),
rebuild_all,
scale_factor,
);
parent_state.merge_up(state.item);
},
);
}

fn build_access_node(
widget: &dyn Widget,
state: &WidgetState,
state_children: ArenaMutChildren<'_, WidgetState>,
scale_factor: f64,
) -> NodeBuilder {
let mut node = NodeBuilder::new(widget.accessibility_role());
node.set_bounds(to_accesskit_rect(state.window_layout_rect(), scale_factor));

// TODO - We skip listing stashed items.
// This may have knock-on effects we'd need to document.
node.set_children(
widget
.children_ids()
.iter()
.copied()
.filter(|id| {
!state_children
.get_child(id.to_raw())
.unwrap()
.item
.is_stashed
})
.map(|id| id.into())
.collect::<Vec<NodeId>>(),
);

if state.is_hot {
node.set_hovered();
}
if state.is_disabled() {
node.set_disabled();
}
if state.is_stashed {
node.set_hidden();
}
if state.clip.is_some() {
node.set_clips_children();
}

node
}

fn to_accesskit_rect(r: Rect, scale_factor: f64) -> accesskit::Rect {
let s = scale_factor;
accesskit::Rect::new(s * r.x0, s * r.y0, s * r.x1, s * r.y1)
}

// ----------------

pub(crate) fn root_accessibility(
root: &mut RenderRoot,
rebuild_all: bool,
scale_factor: f64,
) -> TreeUpdate {
let _span = info_span!("accessibility").entered();

let mut tree_update = TreeUpdate {
nodes: vec![],
tree: None,
focus: root.state.focused_widget.unwrap_or(root.root.id()).into(),
};

let (root_widget, root_state) = {
let widget_id = root.root.id();
let widget = root
.widget_arena
.widgets
.find_mut(widget_id.to_raw())
.expect("root_accessibility: root not in widget tree");
let state = root
.widget_arena
.widget_states
.find_mut(widget_id.to_raw())
.expect("root_accessibility: root state not in widget tree");
(widget, state)
};

if rebuild_all {
debug!("Running ACCESSIBILITY pass with rebuild_all");
}

build_accessibility_tree(
&mut root.state,
&mut tree_update,
root_widget,
root_state,
rebuild_all,
scale_factor,
);

tree_update
}
Loading