Skip to content
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
3 changes: 3 additions & 0 deletions internal/core/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,9 @@ impl Window {
crate::platform::WindowEvent::Resized { size } => {
self.0.set_window_item_geometry(size.to_euclid());
self.0.window_adapter().renderer().resize(size.to_physical(self.scale_factor()))?;
if let Some(item_rc) = self.0.focus_item.borrow().upgrade() {
item_rc.try_scroll_into_visible();
}
}
crate::platform::WindowEvent::CloseRequested => {
if self.0.request_close() {
Expand Down
53 changes: 47 additions & 6 deletions internal/core/item_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ impl ItemRc {
/// Returns an absolute position of `p` in the parent item coordinate system
/// (does not add this item's x and y)
pub fn map_to_window(&self, p: LogicalPoint) -> LogicalPoint {
self.map_to_item_tree_impl(p, None)
self.map_to_item_tree_impl(p, |_| false)
}

/// Returns an absolute position of `p` in the `ItemTree`'s coordinate system
Expand All @@ -480,24 +480,31 @@ impl ItemRc {
p: LogicalPoint,
item_tree: &vtable::VRc<ItemTreeVTable>,
) -> LogicalPoint {
self.map_to_item_tree_impl(p, Some(item_tree))
self.map_to_item_tree_impl(p, |current| current.is_root_item_of(item_tree))
}

/// Returns an absolute position of `p` in the `ancestor`'s coordinate system
/// (does not add this item's x and y)
/// Don't rely on any specific behavior if `self` isn't a descendant of `ancestor`.
fn map_to_ancestor(&self, p: LogicalPoint, ancestor: &Self) -> LogicalPoint {
self.map_to_item_tree_impl(p, |parent| parent == ancestor)
}

fn map_to_item_tree_impl(
&self,
p: LogicalPoint,
item_tree: Option<&vtable::VRc<ItemTreeVTable>>,
stop_condition: impl Fn(&Self) -> bool,
) -> LogicalPoint {
let mut current = self.clone();
let mut result = p;
if item_tree.is_some_and(|item_tree| current.is_root_item_of(item_tree)) {
if stop_condition(&current) {
return result;
}
let supports_transformations = self
.window_adapter()
.is_none_or(|adapter| adapter.renderer().supports_transformations());
while let Some(parent) = current.parent_item(ParentItemTraversalMode::StopAtPopups) {
if item_tree.is_some_and(|item_tree| current.is_root_item_of(item_tree)) {
if stop_condition(&parent) {
break;
}
let geometry = parent.geometry();
Expand All @@ -507,7 +514,7 @@ impl ItemRc {
}
}
result += geometry.origin.to_vector();
current = parent.clone();
current = parent;
}
result
}
Expand Down Expand Up @@ -847,6 +854,40 @@ impl ItemRc {
// Should practically always be possible.
.and_then(|child_transform| child_transform.inverse())
}

pub(crate) fn try_scroll_into_visible(&self) {
let mut parent = self.parent_item(ParentItemTraversalMode::StopAtPopups);
while let Some(item_rc) = parent.as_ref() {
let item_ref = item_rc.borrow();
if let Some(flickable) = vtable::VRef::downcast_pin::<crate::items::Flickable>(item_ref)
{
let geo = self.geometry();

flickable.reveal_points(
item_rc,
&[
self.map_to_ancestor(
LogicalPoint::new(
geo.origin.x - flickable.viewport_x().0,
geo.origin.y - flickable.viewport_y().0,
),
&item_rc,
),
self.map_to_ancestor(
LogicalPoint::new(
geo.max_x() - flickable.viewport_x().0,
geo.max_y() - flickable.viewport_y().0,
),
&item_rc,
),
],
);
break;
}

parent = item_rc.parent_item(ParentItemTraversalMode::StopAtPopups);
}
}
}

impl PartialEq for ItemRc {
Expand Down
77 changes: 77 additions & 0 deletions internal/core/items/flickable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,83 @@ impl ItemConsts for Flickable {
Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
}

impl Flickable {
fn choose_min_move(
current_view_start: Coord, // vx or vy
view_len: Coord, // w or h
content_len: Coord, // vw or vh
points: impl Iterator<Item = Coord>,
) -> Coord {
// Feasible translations t such that for all p: vx+t <= p <= vx+t+w
// -> t in [max_i(p_i - (vx + w)), min_i(p_i - vx)]
let zero = 0 as Coord;
let mut lower = Coord::MIN;
let mut upper = Coord::MAX;

for p in points {
lower = lower.max(p - (current_view_start + view_len));
upper = upper.min(p - current_view_start);
}

if lower > upper {
// No translation can include all points simultaneously; pick nearest bound direction.
// This happens only with NaNs; guard anyway.
return zero;
}

// Allowed translation interval due to scroll limits
let max_scroll = (content_len - view_len).max(zero);
let tmin = -current_view_start; // cannot scroll before 0
let tmax = max_scroll - current_view_start; // cannot scroll past max

let i_min = lower.max(tmin);
let i_max = upper.min(tmax);

if i_min <= i_max {
if zero < i_min {
i_min
} else if zero > i_max {
i_max
} else {
zero
}
// Intervals disjoint: choose closest allowed translation to feasible interval
// either entirely left or right
} else if tmax < lower {
tmax
} else {
tmin
}
}

/// Scroll the Flickable so that all of the points are visible at the same time (if possible).
/// The points have to be in the parent's coordinate space.
pub(crate) fn reveal_points(self: Pin<&Self>, self_rc: &ItemRc, pts: &[LogicalPoint]) {
if pts.is_empty() {
return;
}

// visible viewport size from base Item
let geo = self_rc.geometry();

// content extents and current viewport origin (content coords)
let vw = Self::FIELD_OFFSETS.viewport_width.apply_pin(self).get().0;
let vh = Self::FIELD_OFFSETS.viewport_height.apply_pin(self).get().0;
let vx = -Self::FIELD_OFFSETS.viewport_x.apply_pin(self).get().0;
let vy = -Self::FIELD_OFFSETS.viewport_y.apply_pin(self).get().0;

// choose minimal translation along each axis
let tx = Self::choose_min_move(vx, geo.width(), vw, pts.iter().map(|p| p.x));
let ty = Self::choose_min_move(vy, geo.height(), vh, pts.iter().map(|p| p.y));

let new_vx = vx + tx;
let new_vy = vy + ty;

Self::FIELD_OFFSETS.viewport_x.apply_pin(self).set(euclid::Length::new(-new_vx));
Self::FIELD_OFFSETS.viewport_y.apply_pin(self).set(euclid::Length::new(-new_vy));
}
}

#[repr(C)]
/// Wraps the internal data structure for the Flickable
pub struct FlickableDataBox(core::ptr::NonNull<FlickableData>);
Expand Down
10 changes: 8 additions & 2 deletions internal/core/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -958,11 +958,17 @@ impl WindowInner {
match item {
Some(item) => {
*self.focus_item.borrow_mut() = item.downgrade();
item.borrow().as_ref().focus_event(
let result = item.borrow().as_ref().focus_event(
&FocusEvent::FocusIn(reason),
&self.window_adapter(),
item,
)
);
// Reveal offscreen item when it gains focus
if result == crate::input::FocusEventResult::FocusAccepted {
item.try_scroll_into_visible();
}

result
}
None => {
*self.focus_item.borrow_mut() = Default::default();
Expand Down
80 changes: 80 additions & 0 deletions tests/cases/elements/flickable_focus.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

TestCase := Window {
preferred-width: 500px;
preferred-height: 500px;
no-frame: false;

f := Flickable {
x: 10px;
y: 10px;
width: parent.width - 20px;
height: parent.height - 20px;
viewport_width: 2100px;
viewport_height: 2100px;

VerticalLayout {
padding: 0;
Rectangle {
width: 100%;
height: 20px;
}

HorizontalLayout {
Rectangle {
width: 20px;
height: 100%;
}

inner_ta := TouchArea {
width: 50px;
height: 50px;
fs := FocusScope {
Rectangle { }
}
}
}
}
}

public function set-focus() {
fs.focus();
}
public function remove-focus() {
fs.clear-focus();
}

property <length> offset_x: -f.viewport_x;
property <length> offset_y: -f.viewport_y;
}

/*
```rust
// Test that focused items stay visible when resizing the window

use slint::{LogicalSize, LogicalPosition, platform::WindowEvent};
let instance = TestCase::new().unwrap();
instance.window().dispatch_event(WindowEvent::Resized { size: LogicalSize::new(70., 70.) });
assert_eq!(instance.get_offset_x(), 0.);
assert_eq!(instance.get_offset_y(), 0.);
instance.window().dispatch_event(WindowEvent::Resized { size: LogicalSize::new(500., 500.) });
assert_eq!(instance.get_offset_x(), 0.);
assert_eq!(instance.get_offset_y(), 0.);
instance.invoke_set_focus();
instance.window().dispatch_event(WindowEvent::Resized { size: LogicalSize::new(70., 70.) });
assert_eq!(instance.get_offset_x(), 20.);
assert_eq!(instance.get_offset_y(), 20.);

// Test that items that gain focus scroll to visible

instance.invoke_remove_focus();
instance.window().dispatch_event(WindowEvent::PointerScrolled { position: LogicalPosition::new(25.0, 25.0), delta_x: 20.0, delta_y: 20.0 });
assert_eq!(instance.get_offset_x(), 0.);
assert_eq!(instance.get_offset_y(), 0.);
instance.invoke_set_focus();
assert_eq!(instance.get_offset_x(), 20.);
assert_eq!(instance.get_offset_y(), 20.);

```
*/
Loading