Skip to content

Commit

Permalink
Implement update_scrolls pass (#550)
Browse files Browse the repository at this point in the history
This is part of the Pass Specification RFC:
linebender/rfcs#7

Note: This PRs comes with a lot of new TODO items. Addressing most of
these items is difficult without major refactors, because Portal code
deals with accessing values across multiple widgets, which is still hard
to do elegantly.
  • Loading branch information
PoignardAzur authored Sep 9, 2024
1 parent 75d5156 commit 2ae7326
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 51 deletions.
17 changes: 14 additions & 3 deletions masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ pub struct EventCtx<'a> {
pub(crate) widget_children: ArenaMutChildren<'a, Box<dyn Widget>>,
pub(crate) allow_pointer_capture: bool,
pub(crate) is_handled: bool,
pub(crate) request_pan_to_child: Option<Rect>,
}

/// A context provided to the [`lifecycle`] method on widgets.
Expand Down Expand Up @@ -600,8 +599,20 @@ impl EventCtx<'_> {
}

/// Send a signal to parent widgets to scroll this widget into view.
pub fn request_pan_to_this(&mut self) {
self.request_pan_to_child = Some(self.widget_state.layout_rect());
pub fn request_scroll_to_this(&mut self) {
let rect = self.widget_state.layout_rect();
self.global_state
.scroll_request_targets
.push((self.widget_state.id, rect));
}

/// Send a signal to parent widgets to scroll this area into view.
///
/// `rect` is in local coordinates.
pub fn request_scroll_to(&mut self, rect: Rect) {
self.global_state
.scroll_request_targets
.push((self.widget_state.id, rect));
}

// TODO - Remove
Expand Down
39 changes: 0 additions & 39 deletions masonry/src/passes/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ fn run_event_pass<E>(
widget_children: widget_mut.children,
allow_pointer_capture,
is_handled: false,
request_pan_to_child: None,
};
let widget = widget_mut.item;

Expand Down Expand Up @@ -194,41 +193,3 @@ pub(crate) fn root_on_access_event(

handled
}

// These functions were carved out of WidgetPod code during a previous refactor
// The general "pan to child" logic needs to be added back in.
#[cfg(FALSE)]
fn pan_to_child() {
// TODO - there's some dubious logic here
if let Some(target_rect) = inner_ctx.request_pan_to_child {
self.pan_to_child(parent_ctx, target_rect);
let (state, _) = parent_ctx
.widget_state_children
.get_child_mut(id)
.expect("WidgetPod: inner widget not found in widget tree");
let new_rect = target_rect.with_origin(target_rect.origin() + state.origin.to_vec2());
parent_ctx.request_pan_to_child = Some(new_rect);
}
}

#[cfg(FALSE)]
fn pan_to_child(&mut self, parent_ctx: &mut EventCtx, rect: Rect) {
let id = self.id().to_raw();
let (widget, widget_token) = parent_ctx
.widget_children
.get_child_mut(id)
.expect("WidgetPod: inner widget not found in widget tree");
let (state, state_token) = parent_ctx
.widget_state_children
.get_child_mut(id)
.expect("WidgetPod: inner widget not found in widget tree");
let mut inner_ctx = LifeCycleCtx {
global_state: parent_ctx.global_state,
widget_state: state,
widget_state_children: state_token,
widget_children: widget_token,
};
let event = LifeCycle::RequestPanToChild(rect);

widget.lifecycle(&mut inner_ctx, &event);
}
28 changes: 28 additions & 0 deletions masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,31 @@ pub(crate) fn run_update_disabled_pass(root: &mut RenderRoot) {
let (root_widget, root_state) = root.widget_arena.get_pair_mut(root.root.id());
update_disabled_for_widget(&mut root.state, root_widget, root_state, false);
}

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

// This pass will update scroll positions in cases where a widget has requested to be
// scrolled into view (usually a textbox getting text events).
// Each parent that implements scrolling will update its scroll position to ensure the
// child is visible. (If the target area is larger than the parent, the parent will try
// to show the top left of that area.)
pub(crate) fn run_update_scroll_pass(root: &mut RenderRoot) {
let _span = info_span!("update_scroll").entered();

let scroll_request_targets = std::mem::take(&mut root.state.scroll_request_targets);
for (target, rect) in scroll_request_targets {
let mut target_rect = rect;

run_targeted_update_pass(root, Some(target), |widget, ctx| {
let event = LifeCycle::RequestPanToChild(rect);
widget.lifecycle(ctx, &event);

// TODO - We should run the compose method after this, so
// translations are updated and the rect passed to parents
// is more accurate.

let state = &ctx.widget_state;
target_rect = target_rect + state.translation + state.origin.to_vec2();
});
}
}
13 changes: 11 additions & 2 deletions masonry/src/render_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use accesskit::{ActionRequest, Tree, TreeUpdate};
use parley::fontique::{self, Collection, CollectionOptions};
use parley::{FontContext, LayoutContext};
use tracing::{info_span, warn};
use vello::kurbo::{self, Point};
use vello::kurbo::{self, Point, Rect};
use vello::Scene;

#[cfg(not(target_arch = "wasm32"))]
Expand All @@ -24,7 +24,9 @@ use crate::passes::compose::root_compose;
use crate::passes::event::{root_on_access_event, root_on_pointer_event, root_on_text_event};
use crate::passes::mutate::{mutate_widget, run_mutate_pass};
use crate::passes::paint::root_paint;
use crate::passes::update::{run_update_disabled_pass, run_update_pointer_pass};
use crate::passes::update::{
run_update_disabled_pass, run_update_pointer_pass, run_update_scroll_pass,
};
use crate::text::TextBrush;
use crate::tree_arena::TreeArena;
use crate::widget::WidgetArena;
Expand Down Expand Up @@ -55,11 +57,13 @@ pub struct RenderRoot {
pub(crate) widget_arena: WidgetArena,
}

// TODO - Document these fields.
pub(crate) struct RenderRootState {
pub(crate) debug_logger: DebugLogger,
pub(crate) signal_queue: VecDeque<RenderRootSignal>,
pub(crate) focused_widget: Option<WidgetId>,
pub(crate) next_focused_widget: Option<WidgetId>,
pub(crate) scroll_request_targets: Vec<(WidgetId, Rect)>,
pub(crate) hovered_path: Vec<WidgetId>,
pub(crate) pointer_capture_target: Option<WidgetId>,
pub(crate) cursor_icon: CursorIcon,
Expand Down Expand Up @@ -132,6 +136,7 @@ impl RenderRoot {
signal_queue: VecDeque::new(),
focused_widget: None,
next_focused_widget: None,
scroll_request_targets: Vec::new(),
hovered_path: Vec::new(),
pointer_capture_target: None,
cursor_icon: CursorIcon::Default,
Expand Down Expand Up @@ -547,6 +552,10 @@ impl RenderRoot {
self.state.debug_logger.layout_tree.root = Some(self.root.id().to_raw() as u32);
}

if !self.state.scroll_request_targets.is_empty() {
run_update_scroll_pass(self);
}

if self.root_state().needs_compose && !self.root_state().needs_layout {
root_compose(self, widget_state);
}
Expand Down
49 changes: 43 additions & 6 deletions masonry/src/widget/portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ impl<W: Widget> Portal<W> {
false
}
}

// Note - Rect is in child coordinates
// TODO - Merge with pan_viewport_to
// Right now these functions are just different enough to be a pain to merge.
fn pan_viewport_to_raw(&mut self, portal_size: Size, content_size: Size, target: Rect) -> bool {
let viewport = Rect::from_origin_size(self.viewport_pos, portal_size);

let new_pos_x = compute_pan_range(
viewport.min_x()..viewport.max_x(),
target.min_x()..target.max_x(),
)
.start;
let new_pos_y = compute_pan_range(
viewport.min_y()..viewport.max_y(),
target.min_y()..target.max_y(),
)
.start;

self.set_viewport_pos_raw(portal_size, content_size, Point::new(new_pos_x, new_pos_y))
}
}

// --- MARK: WIDGETMUT ---
Expand Down Expand Up @@ -315,8 +335,29 @@ impl<W: Widget> Widget for Portal<W> {
LifeCycle::WidgetAdded => {
ctx.register_as_portal();
}
//TODO
//LifeCycle::RequestPanToChild(target_rect) => {}
LifeCycle::RequestPanToChild(target) => {
let portal_size = ctx.size();
let content_size = ctx.get_raw_ref(&mut self.child).ctx().layout_rect().size();

self.pan_viewport_to_raw(portal_size, content_size, *target);
ctx.request_compose();

// TODO - There's a lot of code here that's duplicated from the `MouseWheel`
// event in `on_pointer_event`.
// Because this code directly manipulates child widgets, it's hard to factor
// it out.
let mut scrollbar = ctx.get_raw_mut(&mut self.scrollbar_vertical);
scrollbar.widget().cursor_progress =
self.viewport_pos.y / (content_size - portal_size).height;
scrollbar.ctx().request_paint();

std::mem::drop(scrollbar);

let mut scrollbar = ctx.get_raw_mut(&mut self.scrollbar_horizontal);
scrollbar.widget().cursor_progress =
self.viewport_pos.x / (content_size - portal_size).width;
scrollbar.ctx().request_paint();
}
_ => {}
}

Expand Down Expand Up @@ -424,10 +465,6 @@ impl<W: Widget> Widget for Portal<W> {
}

ctx.current_node().set_clips_children();
ctx.current_node()
.push_child(self.scrollbar_horizontal.id().into());
ctx.current_node()
.push_child(self.scrollbar_vertical.id().into());
}

fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
Expand Down
8 changes: 7 additions & 1 deletion masonry/src/widget/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use vello::{
peniko::{BlendMode, Color},
Scene,
};
use winit::event::Ime;

use crate::widget::{LineBreaking, WidgetMut};
use crate::{
Expand Down Expand Up @@ -208,8 +209,13 @@ impl Widget for Textbox {

fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {
let result = self.editor.text_event(ctx, event);
// If focused on a link and enter pressed, follow it?
if result.is_handled() {
// Some platforms will send a lot of spurious Preedit events.
// We only want to request a scroll on user input.
if !matches!(event, TextEvent::Ime(Ime::Preedit(preedit, ..)) if preedit.is_empty()) {
// TODO - Use request_scroll_to with cursor rect
ctx.request_scroll_to_this();
}
ctx.set_handled();
// TODO: only some handlers need this repaint
ctx.request_layout();
Expand Down

0 comments on commit 2ae7326

Please sign in to comment.