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 update_scrolls pass #550

Merged
merged 4 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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 {
Copy link
Member

Choose a reason for hiding this comment

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

Does the order matter here? Is it meaningful to document that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The order doesn't matter. scroll_request_target was initially an option, and I turned it to a Vec just to cover hypothetical cases where someone would want to pan several scroll areas at once.

I guess several targets could interfere if they're in the same scroll region. I think this will be rare enough that we won't need to document it.

let mut target_rect = rect;

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

Copy link
Member

Choose a reason for hiding this comment

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

How will smooth scroll would be handled?

The best logic I can see if to "get where the widget will be", and all the parents scroll to that.

Copy link
Contributor Author

@PoignardAzur PoignardAzur Aug 26, 2024

Choose a reason for hiding this comment

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

Depends what you mean by smooth scroll. Do you mean on a touchscreen?

I think the most straightforward solution is to have CSS-style animatable properties, and have scroll progress be one of them.

Copy link
Member

Choose a reason for hiding this comment

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

I don't mean on a touchscreen, I mean scrolling to a position over a certain amount of time.

Right, yeah. This logic would have to change at that point, but it's also deliberately unaddressed at the moment.

// 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;
Copy link
Member

Choose a reason for hiding this comment

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

So we prefer the top-left of the rectangle to go into view. That seems reasonable, but should probably be documented.

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 added in a comment on the pass function. I'll do a more thorough documentation pass later.


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 @@ -205,8 +206,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(..))) {
Copy link
Member

Choose a reason for hiding this comment

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

Ooh, apparently we can do

Suggested change
if !matches!(event, TextEvent::Ime(Ime::Preedit(..))) {
if !matches!(event, TextEvent::Ime(Ime::Preedit(preedit, ..)) if preedit.is_empty()) {

which in my testing has better behaviour (but is not the easiest to reason about in its current form)

// 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