Skip to content
Open
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/portal_button_list_no_scroll.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/portal_button_list_scroll_to_item_13.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/portal_button_list_scroll_to_item_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/portal_button_list_scrolled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/portal_scrolled_button_into_view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/scrollbar_bottom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/scrollbar_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/scrollbar_down.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/scrollbar_horizontal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added masonry/screenshots/scrollbar_horizontal_hovered.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added masonry/screenshots/scrollbar_hovered.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions masonry/src/widgets/portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ use crate::kurbo::{Axis, Point, Rect, Size, Vec2};
use crate::layout::{LayoutSize, LenDef, LenReq, SizeDef};
use crate::widgets::ScrollBar;

// TODO: make this configurable or move to theme
const SCROLLBAR_ANIM_OVER_MILLIS: f32 = 300.;

// TODO - refactor - see https://github.com/linebender/xilem/issues/366
// TODO - rename "Portal" to "ScrollPortal"?
// TODO - Document which cases need request_layout, request_compose and request_render
Expand Down Expand Up @@ -54,6 +57,7 @@ pub struct Portal<W: Widget + ?Sized> {
scrollbar_horizontal_visible: bool,
scrollbar_vertical: WidgetPod<ScrollBar>,
scrollbar_vertical_visible: bool,
nanos_since_last_pointer_move: Option<u64>,
}

// --- MARK: BUILDERS
Expand All @@ -72,6 +76,7 @@ impl<W: Widget + ?Sized> Portal<W> {
scrollbar_horizontal_visible: false,
scrollbar_vertical: WidgetPod::new(ScrollBar::new(Axis::Vertical, 0.0, 0.0)),
scrollbar_vertical_visible: false,
nanos_since_last_pointer_move: None,
}
}

Expand Down Expand Up @@ -441,6 +446,19 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for Portal<W> {
ctx.set_handled();
};
}
PointerEvent::Move(_) => {
let f = |mut bar: WidgetMut<'_, ScrollBar>| {
if bar.widget.opacity.value() == 0. {
bar.widget.opacity.move_to(1., SCROLLBAR_ANIM_OVER_MILLIS);
bar.ctx.request_anim_frame();
}
};
ctx.mutate_child_later(&mut self.scrollbar_horizontal, f);
ctx.mutate_child_later(&mut self.scrollbar_vertical, f);

self.nanos_since_last_pointer_move = Some(0);
ctx.request_anim_frame();
}
_ => (),
}

Expand Down Expand Up @@ -620,6 +638,31 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for Portal<W> {
}
}

fn on_anim_frame(
&mut self,
ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
interval: u64,
) {
if let Some(mut since_last_move) = self.nanos_since_last_pointer_move.take() {
since_last_move += interval;

// TODO: make this configurable or move to theme
const VISIBILITY_TIMEOUT: u64 = 400_000_000;
if since_last_move >= VISIBILITY_TIMEOUT {
let f = |mut bar: WidgetMut<'_, ScrollBar>| {
bar.widget.opacity.move_to(0., SCROLLBAR_ANIM_OVER_MILLIS);
bar.ctx.request_anim_frame();
};
ctx.mutate_child_later(&mut self.scrollbar_horizontal, f);
ctx.mutate_child_later(&mut self.scrollbar_vertical, f);
} else {
self.nanos_since_last_pointer_move = Some(since_last_move);
ctx.request_anim_frame();
}
}
}

fn register_children(&mut self, ctx: &mut RegisterCtx<'_>) {
ctx.register_child(&mut self.child);
ctx.register_child(&mut self.scrollbar_horizontal);
Expand Down Expand Up @@ -927,6 +970,10 @@ mod tests {

assert_render_snapshot!(harness, "portal_button_list_no_scroll");

harness.mouse_move((200., 200.));
harness.animate_ms(300);
assert_render_snapshot!(harness, "portal_button_list_mouse_jiggle");

harness.edit_root_widget(|mut portal| {
Portal::set_viewport_pos(&mut portal, Point::new(0.0, 130.0))
});
Expand Down
67 changes: 54 additions & 13 deletions masonry/src/widgets/scroll_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ use crate::core::keyboard::{Key, KeyState, NamedKey};
use crate::core::{
AccessCtx, AccessEvent, AllowRawMut, ChildrenIds, EventCtx, LayoutCtx, MeasureCtx, NoAction,
PaintCtx, PointerButtonEvent, PointerEvent, PointerUpdate, PropertiesMut, PropertiesRef,
RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, WidgetMut,
RegisterCtx, TextEvent, UpdateCtx, Widget, WidgetId, WidgetMut,
};
use crate::imaging::Painter;
use crate::imaging::{Composite, GroupRef, Painter};
use crate::kurbo::{Axis, Point, Rect, Size, Stroke};
use crate::layout::LenReq;
use crate::theme;
use crate::widgets::AnimatedF32;

// TODO
// - Fade scrollbars? Find out how Linux/macOS/Windows do it
Expand Down Expand Up @@ -45,6 +46,7 @@ pub struct ScrollBar {
pub(crate) moved: bool,
pub(crate) portal_size: f64,
pub(crate) content_size: f64,
pub(crate) opacity: AnimatedF32,
grab_anchor: Option<f64>,
}

Expand All @@ -61,6 +63,7 @@ impl ScrollBar {
moved: false,
portal_size,
content_size,
opacity: AnimatedF32::stable(0.),
grab_anchor: None,
}
}
Expand Down Expand Up @@ -336,16 +339,23 @@ impl Widget for ScrollBar {
}
}

fn register_children(&mut self, _ctx: &mut RegisterCtx<'_>) {}

fn update(
fn on_anim_frame(
&mut self,
_ctx: &mut UpdateCtx<'_>,
ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &Update,
interval: u64,
) {
let millis = (interval as f64 * 1e-6) as f32;
let result = self.opacity.advance(millis);
ctx.request_paint_only();

if !result.is_completed() {
ctx.request_anim_frame();
}
}

fn register_children(&mut self, _ctx: &mut RegisterCtx<'_>) {}

fn measure(
&mut self,
_ctx: &mut MeasureCtx<'_>,
Expand Down Expand Up @@ -380,16 +390,31 @@ impl Widget for ScrollBar {
_props: &PropertiesRef<'_>,
painter: &mut Painter<'_>,
) {
if self.opacity.value() != 1. {
painter.push_fill_clip(ctx.border_box());
painter.push_group(GroupRef::new().with_composite(Composite::new(
crate::peniko::BlendMode::default(),
self.opacity.value(),
)));
}

let radius = theme::SCROLLBAR_RADIUS;
let edge_width = theme::SCROLLBAR_EDGE_WIDTH;
let cursor_padding = theme::SCROLLBAR_PAD;
let cursor_min_length = theme::SCROLLBAR_MIN_SIZE;
let scrollbar_width = theme::SCROLLBAR_WIDTH;

let size = ctx.content_box_size();
let (inset_x, inset_y) = self.axis.pack_xy(0.0, cursor_padding);
let inset_start = if ctx.is_hovered() || self.grab_anchor.is_some() {
cursor_padding
} else {
cursor_padding + scrollbar_width / 2.
};
let (inset_x0, inset_y0) = self.axis.pack_xy(0.0, inset_start);
let (inset_x1, inset_y1) = self.axis.pack_xy(0.0, cursor_padding);
let cursor_rect = self
.cursor_rect(size, cursor_min_length)
.inset((-inset_x, -inset_y))
.inset((-inset_x0, -inset_y0, -inset_x1, -inset_y1))
.to_rounded_rect(radius);

painter.fill(cursor_rect, theme::SCROLLBAR_COLOR).draw();
Expand All @@ -400,6 +425,11 @@ impl Widget for ScrollBar {
theme::SCROLLBAR_BORDER_COLOR,
)
.draw();

if self.opacity.value() != 1. {
painter.pop_group();
painter.pop_clip();
}
}

fn accessibility_role(&self) -> Role {
Expand Down Expand Up @@ -473,14 +503,21 @@ mod tests {

#[test]
fn simple_scrollbar() {
let widget = NewWidget::new(ScrollBar::new(Axis::Vertical, 200.0, 600.0))
.with_props(Dimensions::FIT);
let mut widget = ScrollBar::new(Axis::Vertical, 200.0, 600.0);
widget.opacity.move_to(1., 0.);
let widget = NewWidget::new(widget).with_props(Dimensions::FIT);

let mut harness = TestHarness::create_with_size(test_property_set(), widget, (50, 200));
let scrollbar_id = harness.root_id();

assert_render_snapshot!(harness, "scrollbar_default");

harness.mouse_move((5., 50.));
assert_render_snapshot!(harness, "scrollbar_hovered");

harness.mouse_move((5., 50.));
assert_render_snapshot!(harness, "scrollbar_hovered");

assert!(harness.pop_action_erased().is_none());

harness.mouse_click_on(scrollbar_id, None);
Expand All @@ -500,14 +537,18 @@ mod tests {

#[test]
fn horizontal_scrollbar() {
let widget = NewWidget::new(ScrollBar::new(Axis::Horizontal, 200.0, 600.0))
.with_props(Dimensions::FIT);
let mut widget = ScrollBar::new(Axis::Horizontal, 200.0, 600.0);
widget.opacity.move_to(1., 0.);
let widget = NewWidget::new(widget).with_props(Dimensions::FIT);

let mut harness = TestHarness::create_with_size(test_property_set(), widget, (200, 50));
let scrollbar_id = harness.root_id();

assert_render_snapshot!(harness, "scrollbar_horizontal");

harness.mouse_move((50., 5.));
assert_render_snapshot!(harness, "scrollbar_horizontal_hovered");

assert!(harness.pop_action_erased().is_none());

harness.mouse_click_on(scrollbar_id, None);
Expand Down
10 changes: 10 additions & 0 deletions masonry/src/widgets/variable_label.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ impl AnimatedF32 {
AnimationStatus::Ongoing
}
}

/// Returns the target value.
pub fn target(&self) -> f32 {
self.target
}

/// Returns the current value.
pub fn value(&self) -> f32 {
self.value
}
}

/// The status an animation can be in.
Expand Down
Loading