feat: animate scrolling workspace viewports
This commit is contained in:
1 parent
f0862348c9
commit
ed529ae900
9 files changed
+623
-52
No files matched your search
@@ -56,6 +56,83 @@ pub struct AnimatedRect {
|
||||
pub velocity: RectVelocity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AnimatedValue {
|
||||
pub current: f64,
|
||||
pub target: f64,
|
||||
pub velocity: f64,
|
||||
}
|
||||
|
||||
impl AnimatedValue {
|
||||
pub fn new(value: f64) -> Self {
|
||||
let value = finite_or_zero(value);
|
||||
|
||||
Self {
|
||||
current: value,
|
||||
target: value,
|
||||
velocity: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_target(&mut self, target: f64) {
|
||||
self.target = finite_or_zero(target);
|
||||
}
|
||||
|
||||
pub fn retarget_preserving_motion(&mut self, target: f64) {
|
||||
let target = finite_or_zero(target);
|
||||
let direction = (target - self.current).signum();
|
||||
|
||||
if direction != 0.0 && self.velocity.signum() != direction {
|
||||
self.velocity *= 0.35;
|
||||
}
|
||||
self.target = target;
|
||||
}
|
||||
|
||||
pub fn snap(&mut self) {
|
||||
self.current = self.target;
|
||||
self.velocity = 0.0;
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, delta: Duration, config: SpringConfig) -> bool {
|
||||
let config = config.normalized();
|
||||
if (self.current - self.target).abs() <= config.position_tolerance
|
||||
&& self.velocity.abs() <= config.velocity_tolerance
|
||||
{
|
||||
self.snap();
|
||||
return false;
|
||||
}
|
||||
|
||||
let seconds = delta.min(MAX_FRAME_DELTA).as_secs_f64();
|
||||
let steps = (seconds / MAX_STEP_SECONDS).ceil().max(1.0) as usize;
|
||||
let step = seconds / steps as f64;
|
||||
|
||||
for _ in 0..steps {
|
||||
let previous_error = self.current - self.target;
|
||||
advance_value(
|
||||
&mut self.current,
|
||||
self.target,
|
||||
&mut self.velocity,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
let current_error = self.current - self.target;
|
||||
if previous_error != 0.0 && previous_error.signum() != current_error.signum() {
|
||||
self.snap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.current - self.target).abs() <= config.position_tolerance
|
||||
&& self.velocity.abs() <= config.velocity_tolerance
|
||||
{
|
||||
self.snap();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimatedRect {
|
||||
pub fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
@@ -276,6 +353,10 @@ fn advance_value(
|
||||
}
|
||||
}
|
||||
|
||||
fn finite_or_zero(value: f64) -> f64 {
|
||||
if value.is_finite() { value } else { 0.0 }
|
||||
}
|
||||
|
||||
fn normalized_rect(rect: Rect) -> Rect {
|
||||
Rect::new(
|
||||
finite_or(rect.x, 0.0),
|
||||
@@ -326,6 +407,48 @@ fn finite_or(value: f64, fallback: f64) -> f64 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scalar_spring_retargets_from_current_motion() {
|
||||
let mut value = AnimatedValue::new(0.0);
|
||||
value.set_target(1_000.0);
|
||||
value.advance(Duration::from_millis(40), SpringConfig::default());
|
||||
let current = value.current;
|
||||
let velocity = value.velocity;
|
||||
|
||||
value.retarget_preserving_motion(2_000.0);
|
||||
|
||||
assert_eq!(value.current, current);
|
||||
assert_eq!(value.velocity, velocity);
|
||||
assert_eq!(value.target, 2_000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_spring_softens_opposing_velocity_on_reversal() {
|
||||
let mut value = AnimatedValue::new(0.0);
|
||||
value.set_target(1_000.0);
|
||||
value.advance(Duration::from_millis(40), SpringConfig::default());
|
||||
let current = value.current;
|
||||
let velocity = value.velocity;
|
||||
|
||||
value.retarget_preserving_motion(-500.0);
|
||||
|
||||
assert!(value.velocity > 0.0);
|
||||
assert_eq!(value.velocity, velocity * 0.35);
|
||||
assert_eq!(value.current, current);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_spring_does_not_overshoot_target() {
|
||||
let mut value = AnimatedValue::new(0.0);
|
||||
value.velocity = 20_000.0;
|
||||
value.set_target(100.0);
|
||||
|
||||
for _ in 0..60 {
|
||||
value.advance(Duration::from_secs_f64(1.0 / 60.0), SpringConfig::default());
|
||||
assert!(value.current <= 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_target_changes_without_jumping_visual_geometry() {
|
||||
let initial = Rect::new(0.0, 0.0, 400.0, 300.0);
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::fmt;
|
||||
|
||||
use ferese_layout::{
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect,
|
||||
ScrollingLayout, SizeConstraints, WindowId,
|
||||
ScrollingLayout, SizeConstraints, ViewportFocusStrategy, WindowId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -40,10 +40,16 @@ impl Default for WorkspaceLayout {
|
||||
}
|
||||
|
||||
impl WorkspaceLayout {
|
||||
pub fn new(mode: LayoutMode, default_column_width: ColumnWidth) -> Self {
|
||||
pub fn new(
|
||||
mode: LayoutMode,
|
||||
default_column_width: ColumnWidth,
|
||||
focus_strategy: ViewportFocusStrategy,
|
||||
) -> Self {
|
||||
match mode {
|
||||
LayoutMode::Scrolling => {
|
||||
Self::Scrolling(ScrollingLayout::with_default_width(default_column_width))
|
||||
let mut layout = ScrollingLayout::with_default_width(default_column_width);
|
||||
layout.set_focus_strategy(focus_strategy);
|
||||
Self::Scrolling(layout)
|
||||
}
|
||||
LayoutMode::Tree => Self::Tree(LayoutTree::default()),
|
||||
}
|
||||
@@ -56,6 +62,20 @@ impl WorkspaceLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewport_x(&self) -> Option<f64> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => Some(layout.viewport_x()),
|
||||
Self::Tree(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preferred_window(&self) -> Option<WindowId> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => layout.active_window(),
|
||||
Self::Tree(layout) => layout.window_ids().next(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, window: WindowId) -> bool {
|
||||
match self {
|
||||
Self::Scrolling(layout) => layout.contains(window),
|
||||
@@ -235,6 +255,7 @@ impl WorkspaceLayout {
|
||||
bounds: Rect,
|
||||
focused: Option<WindowId>,
|
||||
default_column_width: ColumnWidth,
|
||||
focus_strategy: ViewportFocusStrategy,
|
||||
) -> Result<bool, LayoutError> {
|
||||
if self.mode() == mode {
|
||||
return Ok(false);
|
||||
@@ -265,6 +286,7 @@ impl WorkspaceLayout {
|
||||
Self::Tree(layout) => {
|
||||
let windows = layout.window_ids_in_reading_order(bounds)?;
|
||||
let mut scrolling = ScrollingLayout::with_default_width(default_column_width);
|
||||
scrolling.set_focus_strategy(focus_strategy);
|
||||
let mut previous = None;
|
||||
|
||||
for window in windows {
|
||||
@@ -339,22 +361,35 @@ pub struct WorkspaceSet {
|
||||
placements: HashMap<WindowId, WindowPlacement>,
|
||||
default_layout_mode: LayoutMode,
|
||||
default_column_width: ColumnWidth,
|
||||
scrolling_focus_strategy: ViewportFocusStrategy,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl Default for WorkspaceSet {
|
||||
fn default() -> Self {
|
||||
Self::new(LayoutMode::default(), ColumnWidth::default())
|
||||
Self::new(
|
||||
LayoutMode::default(),
|
||||
ColumnWidth::default(),
|
||||
ViewportFocusStrategy::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceSet {
|
||||
pub fn new(default_layout_mode: LayoutMode, default_column_width: ColumnWidth) -> Self {
|
||||
pub fn new(
|
||||
default_layout_mode: LayoutMode,
|
||||
default_column_width: ColumnWidth,
|
||||
scrolling_focus_strategy: ViewportFocusStrategy,
|
||||
) -> Self {
|
||||
let active = WorkspaceId(1);
|
||||
let workspace = Workspace {
|
||||
id: active,
|
||||
name: "1".to_owned(),
|
||||
layout: WorkspaceLayout::new(default_layout_mode, default_column_width),
|
||||
layout: WorkspaceLayout::new(
|
||||
default_layout_mode,
|
||||
default_column_width,
|
||||
scrolling_focus_strategy,
|
||||
),
|
||||
floating: Vec::new(),
|
||||
last_focused: None,
|
||||
fullscreen: None,
|
||||
@@ -370,6 +405,7 @@ impl WorkspaceSet {
|
||||
placements: HashMap::new(),
|
||||
default_layout_mode,
|
||||
default_column_width,
|
||||
scrolling_focus_strategy,
|
||||
next_id: 2,
|
||||
}
|
||||
}
|
||||
@@ -423,11 +459,16 @@ impl WorkspaceSet {
|
||||
bounds: Rect,
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
let default_column_width = self.default_column_width;
|
||||
let focus_strategy = self.scrolling_focus_strategy;
|
||||
let workspace = self.active_mut();
|
||||
let focused = tiled_focus(workspace);
|
||||
let changed = workspace
|
||||
.layout
|
||||
.set_mode(mode, bounds, focused, default_column_width)?;
|
||||
let changed = workspace.layout.set_mode(
|
||||
mode,
|
||||
bounds,
|
||||
focused,
|
||||
default_column_width,
|
||||
focus_strategy,
|
||||
)?;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(changed)
|
||||
@@ -588,7 +629,11 @@ impl WorkspaceSet {
|
||||
Workspace {
|
||||
id,
|
||||
name,
|
||||
layout: WorkspaceLayout::new(self.default_layout_mode, self.default_column_width),
|
||||
layout: WorkspaceLayout::new(
|
||||
self.default_layout_mode,
|
||||
self.default_column_width,
|
||||
self.scrolling_focus_strategy,
|
||||
),
|
||||
floating: Vec::new(),
|
||||
last_focused: None,
|
||||
fullscreen: None,
|
||||
@@ -914,11 +959,13 @@ impl WorkspaceSet {
|
||||
}
|
||||
|
||||
fn first_window(workspace: &Workspace) -> Option<WindowId> {
|
||||
workspace
|
||||
.layout
|
||||
.window_ids()
|
||||
.chain(workspace.floating.iter().copied())
|
||||
.min_by_key(|window| window.0)
|
||||
workspace.layout.preferred_window().or_else(|| {
|
||||
workspace
|
||||
.floating
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|window| window.0)
|
||||
})
|
||||
}
|
||||
|
||||
fn tiled_focus(workspace: &Workspace) -> Option<WindowId> {
|
||||
@@ -959,7 +1006,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configured_layout_defaults_apply_to_new_workspaces() {
|
||||
let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full);
|
||||
let mut workspaces = WorkspaceSet::new(
|
||||
LayoutMode::Scrolling,
|
||||
ColumnWidth::Full,
|
||||
ViewportFocusStrategy::Minimal,
|
||||
);
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
@@ -1044,6 +1095,21 @@ mod tests {
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_scrolling_column_focuses_its_adjacent_column() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=4 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
}
|
||||
workspaces.focus_window(WindowId(3)).unwrap();
|
||||
|
||||
workspaces.remove_window(WindowId(3)).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_rejects_a_window_on_an_inactive_workspace() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::fmt;
|
||||
|
||||
mod scrolling;
|
||||
|
||||
pub use scrolling::{Column, ColumnWidth, ScrollingLayout};
|
||||
pub use scrolling::{Column, ColumnWidth, ScrollingLayout, ViewportFocusStrategy};
|
||||
|
||||
const MIN_SPLIT_RATIO: f64 = 0.05;
|
||||
const MAX_SPLIT_RATIO: f64 = 0.95;
|
||||
|
||||
@@ -9,6 +9,8 @@ const DEFAULT_WIDTH: f64 = 0.5;
|
||||
const MIN_COLUMN_WIDTH: f64 = 0.1;
|
||||
const MAX_COLUMN_WIDTH: f64 = 2.0;
|
||||
const MIN_ROW_HEIGHT: f64 = 0.05;
|
||||
const COMFORT_START: f64 = 0.2;
|
||||
const COMFORT_END: f64 = 0.8;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ColumnWidth {
|
||||
@@ -17,6 +19,13 @@ pub enum ColumnWidth {
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum ViewportFocusStrategy {
|
||||
#[default]
|
||||
Minimal,
|
||||
Center,
|
||||
}
|
||||
|
||||
impl Default for ColumnWidth {
|
||||
fn default() -> Self {
|
||||
Self::Proportion(DEFAULT_WIDTH)
|
||||
@@ -67,6 +76,8 @@ pub struct ScrollingLayout {
|
||||
viewport_x: f64,
|
||||
neighbor_context: f64,
|
||||
default_width: ColumnWidth,
|
||||
focus_strategy: ViewportFocusStrategy,
|
||||
reveal_pending: bool,
|
||||
}
|
||||
|
||||
impl Default for ScrollingLayout {
|
||||
@@ -77,6 +88,8 @@ impl Default for ScrollingLayout {
|
||||
viewport_x: 0.0,
|
||||
neighbor_context: 48.0,
|
||||
default_width: ColumnWidth::default(),
|
||||
focus_strategy: ViewportFocusStrategy::Minimal,
|
||||
reveal_pending: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +102,10 @@ impl ScrollingLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focus_strategy(&mut self, strategy: ViewportFocusStrategy) {
|
||||
self.focus_strategy = strategy;
|
||||
}
|
||||
|
||||
pub fn default_width(&self) -> ColumnWidth {
|
||||
self.default_width
|
||||
}
|
||||
@@ -101,6 +118,11 @@ impl ScrollingLayout {
|
||||
self.active_column
|
||||
}
|
||||
|
||||
pub fn active_window(&self) -> Option<WindowId> {
|
||||
let column = self.columns.get(self.active_column?)?;
|
||||
column.windows.get(column.active).copied()
|
||||
}
|
||||
|
||||
pub fn viewport_x(&self) -> f64 {
|
||||
self.viewport_x
|
||||
}
|
||||
@@ -131,6 +153,7 @@ impl ScrollingLayout {
|
||||
self.columns
|
||||
.insert(index, Column::new(window, self.default_width));
|
||||
self.active_column = Some(index);
|
||||
self.reveal_pending = self.columns.len() > 1;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(())
|
||||
@@ -141,6 +164,8 @@ impl ScrollingLayout {
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let column = &mut self.columns[column_index];
|
||||
let removed_active_column =
|
||||
self.active_column == Some(column_index) && column.windows.len() == 1;
|
||||
column.windows.remove(window_index);
|
||||
column.heights.remove(window_index);
|
||||
|
||||
@@ -156,6 +181,7 @@ impl ScrollingLayout {
|
||||
column.active = column.active.min(column.windows.len() - 1);
|
||||
column.normalize_heights();
|
||||
}
|
||||
self.reveal_pending |= removed_active_column && !self.columns.is_empty();
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(())
|
||||
@@ -165,8 +191,10 @@ impl ScrollingLayout {
|
||||
let (column, index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let changed_column = self.active_column != Some(column);
|
||||
self.active_column = Some(column);
|
||||
self.columns[column].active = index;
|
||||
self.reveal_pending |= changed_column;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -191,6 +219,7 @@ impl ScrollingLayout {
|
||||
column.active = insertion;
|
||||
column.normalize_heights();
|
||||
self.active_column = Some(target_column);
|
||||
self.reveal_pending = true;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(())
|
||||
@@ -210,6 +239,7 @@ impl ScrollingLayout {
|
||||
self.columns
|
||||
.insert(insertion, Column::new(window, self.default_width));
|
||||
self.active_column = Some(insertion);
|
||||
self.reveal_pending = true;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(())
|
||||
@@ -405,7 +435,7 @@ impl ScrollingLayout {
|
||||
let viewport_width = (bounds.width - outer * 2.0).max(1.0);
|
||||
let viewport_height = (bounds.height - outer * 2.0).max(1.0);
|
||||
let mut column_positions = Vec::with_capacity(self.columns.len());
|
||||
let mut strip_width = 0.0;
|
||||
let mut next_column_x = 0.0;
|
||||
|
||||
for column in &self.columns {
|
||||
let requested = match normalized_width(column.width) {
|
||||
@@ -428,11 +458,13 @@ impl ScrollingLayout {
|
||||
})
|
||||
.fold(1.0_f64, f64::max);
|
||||
let width = requested.max(minimum).max(1.0);
|
||||
column_positions.push((strip_width, width));
|
||||
strip_width += width + inner;
|
||||
column_positions.push((next_column_x, width));
|
||||
next_column_x += width + inner;
|
||||
}
|
||||
if self.reveal_pending {
|
||||
self.reveal_active_column(&column_positions, viewport_width);
|
||||
self.reveal_pending = false;
|
||||
}
|
||||
strip_width = (strip_width - inner).max(0.0);
|
||||
self.reveal_active_column(&column_positions, strip_width, viewport_width);
|
||||
|
||||
let mut result = LayoutResult::default();
|
||||
for (column_index, column) in self.columns.iter().enumerate() {
|
||||
@@ -464,25 +496,36 @@ impl ScrollingLayout {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn reveal_active_column(
|
||||
&mut self,
|
||||
positions: &[(f64, f64)],
|
||||
strip_width: f64,
|
||||
viewport_width: f64,
|
||||
) {
|
||||
let max_scroll = (strip_width - viewport_width).max(0.0);
|
||||
self.viewport_x = finite_nonnegative(self.viewport_x).min(max_scroll);
|
||||
fn reveal_active_column(&mut self, positions: &[(f64, f64)], viewport_width: f64) {
|
||||
let Some(active) = self.active_column else {
|
||||
return;
|
||||
};
|
||||
let (start, width) = positions[active];
|
||||
let end = start + width;
|
||||
let context = finite_nonnegative(self.neighbor_context).min(viewport_width / 3.0);
|
||||
if self.focus_strategy == ViewportFocusStrategy::Center {
|
||||
self.viewport_x = start + width / 2.0 - viewport_width / 2.0;
|
||||
return;
|
||||
}
|
||||
|
||||
if start < self.viewport_x + context {
|
||||
self.viewport_x = (start - context).max(0.0);
|
||||
} else if end > self.viewport_x + viewport_width - context {
|
||||
self.viewport_x = (end - viewport_width + context).min(max_scroll);
|
||||
if start >= self.viewport_x && end <= self.viewport_x + viewport_width {
|
||||
return;
|
||||
}
|
||||
|
||||
let comfort_start = viewport_width * COMFORT_START;
|
||||
let comfort_end = viewport_width * COMFORT_END;
|
||||
let comfort_width = comfort_end - comfort_start;
|
||||
|
||||
if width > comfort_width {
|
||||
let context = finite_nonnegative(self.neighbor_context).min(viewport_width / 3.0);
|
||||
if start > self.viewport_x + comfort_start {
|
||||
self.viewport_x = start - context;
|
||||
} else if end < self.viewport_x + comfort_end {
|
||||
self.viewport_x = end - viewport_width + context;
|
||||
}
|
||||
} else if start < self.viewport_x + comfort_start {
|
||||
self.viewport_x = start - comfort_start;
|
||||
} else if end > self.viewport_x + comfort_end {
|
||||
self.viewport_x = end - comfort_end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +540,7 @@ impl ScrollingLayout {
|
||||
if self.columns[source].windows.len() == 1 {
|
||||
self.columns.swap(source, destination);
|
||||
self.active_column = Some(destination);
|
||||
self.reveal_pending = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -510,6 +554,7 @@ impl ScrollingLayout {
|
||||
self.columns
|
||||
.insert(insertion, Column::new(window, self.default_width));
|
||||
self.active_column = Some(insertion);
|
||||
self.reveal_pending = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -523,7 +568,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), LayoutError> {
|
||||
if !self.viewport_x.is_finite() || self.viewport_x < 0.0 {
|
||||
if !self.viewport_x.is_finite() {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling viewport offset is invalid",
|
||||
));
|
||||
@@ -701,8 +746,8 @@ mod tests {
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(layout.viewport_x(), 1_000.0);
|
||||
assert_eq!(result.geometry[&window(4)].x, 500.0);
|
||||
assert_eq!(layout.viewport_x(), 1_200.0);
|
||||
assert_eq!(result.geometry[&window(4)].x, 300.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -737,6 +782,85 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_resize_does_not_retarget_the_viewport() {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
inner: 0.0,
|
||||
outer: 0.0,
|
||||
smart: false,
|
||||
};
|
||||
|
||||
for id in 1..=3 {
|
||||
layout
|
||||
.insert(window(id), (id > 1).then(|| window(id - 1)))
|
||||
.unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(id)))
|
||||
.unwrap();
|
||||
}
|
||||
let before = layout.viewport_x();
|
||||
|
||||
layout
|
||||
.resize_window(window(3), Direction::Right, 0.5)
|
||||
.unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(layout.viewport_x(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_is_not_clamped_to_the_column_strip() {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
inner: 0.0,
|
||||
outer: 0.0,
|
||||
smart: false,
|
||||
};
|
||||
|
||||
for id in 1..=3 {
|
||||
layout.insert(window(id), None).unwrap();
|
||||
}
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
|
||||
.unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(layout.viewport_x(), -200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_non_focused_column_does_not_move_viewport() {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
inner: 0.0,
|
||||
outer: 0.0,
|
||||
smart: false,
|
||||
};
|
||||
|
||||
for id in 1..=4 {
|
||||
layout.insert(window(id), None).unwrap();
|
||||
}
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
|
||||
.unwrap();
|
||||
let before = layout.viewport_x();
|
||||
|
||||
layout.remove(window(1)).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(layout.viewport_x(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouping_and_extracting_preserve_membership() {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
|
||||
Reference in new issue
Block a user