mirror of https://github.com/linebender/xilem
616 lines
22 KiB
Rust
616 lines
22 KiB
Rust
// Copyright 2020 the Xilem Authors and the Druid Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#![allow(missing_docs)]
|
|
|
|
use std::ops::Range;
|
|
|
|
use accesskit::Role;
|
|
use kurbo::Affine;
|
|
use smallvec::{smallvec, SmallVec};
|
|
use tracing::{trace_span, Span};
|
|
use vello::peniko::BlendMode;
|
|
use vello::Scene;
|
|
|
|
use crate::kurbo::{Point, Rect, Size, Vec2};
|
|
use crate::widget::{Axis, ScrollBar, WidgetMut, WidgetRef};
|
|
use crate::{
|
|
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
|
PointerEvent, StatusChange, TextEvent, Widget, WidgetPod,
|
|
};
|
|
|
|
// TODO - refactor - see issue #15
|
|
// TODO - rename "Portal" to "ScrollPortal"?
|
|
// Conceptually, a Portal is a Widget giving a restricted view of a child widget
|
|
// Imagine a very large widget, and a rect that represents the part of the widget we see
|
|
pub struct Portal<W: Widget> {
|
|
child: WidgetPod<W>,
|
|
// TODO - differentiate between the "explicit" viewport pos determined
|
|
// by user input, and the computed viewport pos that may change based
|
|
// on re-layouts
|
|
// TODO - rename
|
|
viewport_pos: Point,
|
|
// TODO - test how it looks like
|
|
constrain_horizontal: bool,
|
|
constrain_vertical: bool,
|
|
must_fill: bool,
|
|
scrollbar_horizontal: WidgetPod<ScrollBar>,
|
|
scrollbar_horizontal_visible: bool,
|
|
scrollbar_vertical: WidgetPod<ScrollBar>,
|
|
scrollbar_vertical_visible: bool,
|
|
}
|
|
|
|
impl<W: Widget> Portal<W> {
|
|
pub fn new(child: W) -> Self {
|
|
Portal {
|
|
child: WidgetPod::new(child),
|
|
viewport_pos: Point::ORIGIN,
|
|
constrain_horizontal: false,
|
|
constrain_vertical: false,
|
|
must_fill: false,
|
|
// TODO - remove
|
|
scrollbar_horizontal: WidgetPod::new(ScrollBar::new(Axis::Horizontal, 1.0, 1.0)),
|
|
scrollbar_horizontal_visible: false,
|
|
scrollbar_vertical: WidgetPod::new(ScrollBar::new(Axis::Vertical, 1.0, 1.0)),
|
|
scrollbar_vertical_visible: false,
|
|
}
|
|
}
|
|
|
|
pub fn get_viewport_pos(&self) -> Point {
|
|
self.viewport_pos
|
|
}
|
|
|
|
pub fn child(&self) -> WidgetRef<'_, W> {
|
|
self.child.as_ref()
|
|
}
|
|
|
|
// TODO - rewrite doc
|
|
/// Builder-style method for deciding whether to constrain the child vertically.
|
|
///
|
|
/// The default is `false`.
|
|
///
|
|
/// This setting affects how a `ClipBox` lays out its child.
|
|
///
|
|
/// - When it is `false` (the default), the child does not receive any upper
|
|
/// bound on its height: the idea is that the child can be as tall as it
|
|
/// wants, and the viewport will somehow get moved around to see all of it.
|
|
/// - When it is `true`, the viewport's maximum height will be passed down
|
|
/// as an upper bound on the height of the child, and the viewport will set
|
|
/// its own height to be the same as its child's height.
|
|
pub fn constrain_vertical(mut self, constrain: bool) -> Self {
|
|
self.constrain_vertical = constrain;
|
|
self
|
|
}
|
|
|
|
/// Builder-style method for deciding whether to constrain the child horizontally.
|
|
///
|
|
/// The default is `false`. See [`constrain_vertical`] for more details.
|
|
///
|
|
/// [`constrain_vertical`]: struct.ClipBox.html#constrain_vertical
|
|
pub fn constrain_horizontal(mut self, constrain: bool) -> Self {
|
|
self.constrain_horizontal = constrain;
|
|
self
|
|
}
|
|
|
|
/// Builder-style method to set whether the child must fill the view.
|
|
///
|
|
/// If `false` (the default) there is no minimum constraint on the child's
|
|
/// size. If `true`, the child is passed the same minimum constraints as
|
|
/// the `ClipBox`.
|
|
pub fn content_must_fill(mut self, must_fill: bool) -> Self {
|
|
self.must_fill = must_fill;
|
|
self
|
|
}
|
|
}
|
|
|
|
fn compute_pan_range(mut viewport: Range<f64>, target: Range<f64>) -> Range<f64> {
|
|
// if either range contains the other, the viewport doesn't move
|
|
if target.start <= viewport.start && viewport.end <= target.end {
|
|
return viewport;
|
|
}
|
|
if viewport.start <= target.start && target.end <= viewport.end {
|
|
return viewport;
|
|
}
|
|
|
|
// we compute the length that we need to "fit" in our viewport
|
|
let target_width = f64::min(viewport.end - viewport.start, target.end - target.start);
|
|
let viewport_width = viewport.end - viewport.start;
|
|
|
|
// Because of the early returns, there are only two cases to consider: we need
|
|
// to move the viewport "left" or "right"
|
|
if viewport.start >= target.start {
|
|
viewport.start = target.end - target_width;
|
|
viewport.end = viewport.start + viewport_width;
|
|
} else {
|
|
viewport.end = target.start + target_width;
|
|
viewport.start = viewport.end - viewport_width;
|
|
}
|
|
|
|
viewport
|
|
}
|
|
|
|
impl<W: Widget> Portal<W> {
|
|
// TODO - rename
|
|
fn set_viewport_pos_raw(&mut self, portal_size: Size, content_size: Size, pos: Point) -> bool {
|
|
let viewport_max_pos =
|
|
(content_size - portal_size).clamp(Size::ZERO, Size::new(f64::INFINITY, f64::INFINITY));
|
|
let pos = Point::new(
|
|
pos.x.clamp(0.0, viewport_max_pos.width),
|
|
pos.y.clamp(0.0, viewport_max_pos.height),
|
|
);
|
|
|
|
if (pos - self.viewport_pos).hypot2() > 1e-12 {
|
|
self.viewport_pos = pos;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<W: Widget> WidgetMut<'_, Portal<W>> {
|
|
pub fn child_mut(&mut self) -> WidgetMut<'_, W> {
|
|
self.ctx.get_mut(&mut self.widget.child)
|
|
}
|
|
|
|
pub fn horizontal_scrollbar_mut(&mut self) -> WidgetMut<'_, ScrollBar> {
|
|
self.ctx.get_mut(&mut self.widget.scrollbar_horizontal)
|
|
}
|
|
|
|
pub fn vertical_scrollbar_mut(&mut self) -> WidgetMut<'_, ScrollBar> {
|
|
self.ctx.get_mut(&mut self.widget.scrollbar_vertical)
|
|
}
|
|
|
|
// TODO - rewrite doc
|
|
/// Set whether to constrain the child horizontally.
|
|
///
|
|
/// See [`constrain_vertical`] for more details.
|
|
///
|
|
/// [`constrain_vertical`]: struct.ClipBox.html#constrain_vertical
|
|
pub fn set_constrain_horizontal(&mut self, constrain: bool) {
|
|
self.widget.constrain_horizontal = constrain;
|
|
self.ctx.request_layout();
|
|
}
|
|
|
|
/// Set whether to constrain the child vertically.
|
|
///
|
|
/// See [`constrain_vertical`] for more details.
|
|
///
|
|
/// [`constrain_vertical`]: struct.ClipBox.html#constrain_vertical
|
|
pub fn set_constrain_vertical(&mut self, constrain: bool) {
|
|
self.widget.constrain_vertical = constrain;
|
|
self.ctx.request_layout();
|
|
}
|
|
|
|
/// Set whether the child's size must be greater than or equal the size of
|
|
/// the `ClipBox`.
|
|
///
|
|
/// See [`content_must_fill`] for more details.
|
|
///
|
|
/// [`content_must_fill`]: ClipBox::content_must_fill
|
|
pub fn set_content_must_fill(&mut self, must_fill: bool) {
|
|
self.widget.must_fill = must_fill;
|
|
self.ctx.request_layout();
|
|
}
|
|
|
|
pub fn set_viewport_pos(&mut self, position: Point) -> bool {
|
|
let portal_size = self.ctx.widget_state.layout_rect().size();
|
|
let content_size = self.widget.child.layout_rect().size();
|
|
|
|
let pos_changed = self
|
|
.widget
|
|
.set_viewport_pos_raw(portal_size, content_size, position);
|
|
if pos_changed {
|
|
let progress_x = self.widget.viewport_pos.x / (content_size - portal_size).width;
|
|
self.horizontal_scrollbar_mut()
|
|
.set_cursor_progress(progress_x);
|
|
let progress_y = self.widget.viewport_pos.y / (content_size - portal_size).height;
|
|
self.vertical_scrollbar_mut()
|
|
.set_cursor_progress(progress_y);
|
|
self.ctx.request_layout();
|
|
}
|
|
pos_changed
|
|
}
|
|
|
|
pub fn pan_viewport_by(&mut self, translation: Vec2) -> bool {
|
|
self.set_viewport_pos(self.widget.viewport_pos + translation)
|
|
}
|
|
|
|
// Note - Rect is in child coordinates
|
|
pub fn pan_viewport_to(&mut self, target: Rect) -> bool {
|
|
let viewport = Rect::from_origin_size(self.widget.viewport_pos, self.ctx.widget_state.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(Point::new(new_pos_x, new_pos_y))
|
|
}
|
|
}
|
|
|
|
impl<W: Widget> Widget for Portal<W> {
|
|
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {
|
|
let portal_size = ctx.size();
|
|
let content_size = self.child.layout_rect().size();
|
|
|
|
match event {
|
|
PointerEvent::MouseWheel(delta, _) => {
|
|
self.set_viewport_pos_raw(
|
|
portal_size,
|
|
content_size,
|
|
self.viewport_pos + Vec2::new(delta.x, delta.y),
|
|
);
|
|
// TODO - horizontal scrolling?
|
|
ctx.get_mut(&mut self.scrollbar_vertical)
|
|
.set_cursor_progress(self.viewport_pos.y / (content_size - portal_size).height);
|
|
ctx.request_layout();
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
self.child.on_pointer_event(ctx, event);
|
|
self.scrollbar_horizontal.on_pointer_event(ctx, event);
|
|
self.scrollbar_vertical.on_pointer_event(ctx, event);
|
|
|
|
if self.scrollbar_horizontal.widget().moved {
|
|
let progress = self.scrollbar_horizontal.widget().cursor_progress;
|
|
self.scrollbar_horizontal.widget_mut().moved = false;
|
|
self.viewport_pos = Axis::Horizontal
|
|
.pack(
|
|
progress * Axis::Horizontal.major(content_size - portal_size),
|
|
Axis::Horizontal.minor_pos(self.viewport_pos),
|
|
)
|
|
.into();
|
|
ctx.request_layout();
|
|
}
|
|
if self.scrollbar_vertical.widget().moved {
|
|
let progress = self.scrollbar_vertical.widget().cursor_progress;
|
|
self.scrollbar_vertical.widget_mut().moved = false;
|
|
self.viewport_pos = Axis::Vertical
|
|
.pack(
|
|
progress * Axis::Vertical.major(content_size - portal_size),
|
|
Axis::Vertical.minor_pos(self.viewport_pos),
|
|
)
|
|
.into();
|
|
ctx.request_layout();
|
|
}
|
|
}
|
|
|
|
// TODO - handle Home/End keys, etc
|
|
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {
|
|
self.child.on_text_event(ctx, event);
|
|
self.scrollbar_horizontal.on_text_event(ctx, event);
|
|
self.scrollbar_vertical.on_text_event(ctx, event);
|
|
}
|
|
|
|
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
|
|
// TODO - Handle scroll-related events?
|
|
|
|
self.child.on_access_event(ctx, event);
|
|
self.scrollbar_horizontal.on_access_event(ctx, event);
|
|
self.scrollbar_vertical.on_access_event(ctx, event);
|
|
}
|
|
|
|
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
|
|
|
|
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
|
|
match event {
|
|
LifeCycle::WidgetAdded => {
|
|
ctx.register_as_portal();
|
|
}
|
|
//TODO
|
|
//LifeCycle::RequestPanToChild(target_rect) => {}
|
|
_ => {}
|
|
}
|
|
|
|
self.child.lifecycle(ctx, event);
|
|
self.scrollbar_horizontal.lifecycle(ctx, event);
|
|
self.scrollbar_vertical.lifecycle(ctx, event);
|
|
}
|
|
|
|
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
|
|
let min_child_size = if self.must_fill { bc.min() } else { Size::ZERO };
|
|
let mut max_child_size = bc.max();
|
|
if !self.constrain_horizontal {
|
|
max_child_size.width = f64::INFINITY;
|
|
};
|
|
if !self.constrain_vertical {
|
|
max_child_size.height = f64::INFINITY;
|
|
};
|
|
|
|
let child_bc = BoxConstraints::new(min_child_size, max_child_size);
|
|
|
|
let content_size = self.child.layout(ctx, &child_bc);
|
|
let portal_size = bc.constrain(content_size);
|
|
|
|
// TODO - document better
|
|
// Recompute the portal offset for the new layout
|
|
self.set_viewport_pos_raw(portal_size, content_size, self.viewport_pos);
|
|
// TODO - recompute portal progress
|
|
|
|
ctx.place_child(&mut self.child, Point::new(0.0, -self.viewport_pos.y));
|
|
|
|
self.scrollbar_horizontal_visible =
|
|
!self.constrain_horizontal && portal_size.width < content_size.width;
|
|
self.scrollbar_vertical_visible =
|
|
!self.constrain_vertical && portal_size.height < content_size.height;
|
|
|
|
if self.scrollbar_horizontal_visible {
|
|
self.scrollbar_horizontal.widget_mut().portal_size = portal_size.width;
|
|
self.scrollbar_horizontal.widget_mut().content_size = content_size.width;
|
|
let scrollbar_size = self.scrollbar_horizontal.layout(ctx, bc);
|
|
ctx.place_child(
|
|
&mut self.scrollbar_horizontal,
|
|
Point::new(0.0, portal_size.height - scrollbar_size.height),
|
|
);
|
|
} else {
|
|
ctx.skip_child(&mut self.scrollbar_horizontal);
|
|
}
|
|
if self.scrollbar_vertical_visible {
|
|
self.scrollbar_vertical.widget_mut().portal_size = portal_size.height;
|
|
self.scrollbar_vertical.widget_mut().content_size = content_size.height;
|
|
let scrollbar_size = self.scrollbar_vertical.layout(ctx, bc);
|
|
ctx.place_child(
|
|
&mut self.scrollbar_vertical,
|
|
Point::new(portal_size.width - scrollbar_size.width, 0.0),
|
|
);
|
|
} else {
|
|
ctx.skip_child(&mut self.scrollbar_vertical);
|
|
}
|
|
|
|
portal_size
|
|
}
|
|
|
|
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
|
|
// TODO - also clip the invalidated region
|
|
let clip_rect = ctx.size().to_rect();
|
|
|
|
scene.push_layer(BlendMode::default(), 1., Affine::IDENTITY, &clip_rect);
|
|
self.child.paint(ctx, scene);
|
|
scene.pop_layer();
|
|
|
|
if self.scrollbar_horizontal_visible {
|
|
self.scrollbar_horizontal.paint(ctx, scene);
|
|
} else {
|
|
ctx.skip_child(&mut self.scrollbar_horizontal);
|
|
}
|
|
if self.scrollbar_vertical_visible {
|
|
self.scrollbar_vertical.paint(ctx, scene);
|
|
} else {
|
|
ctx.skip_child(&mut self.scrollbar_vertical);
|
|
}
|
|
}
|
|
|
|
fn accessibility_role(&self) -> Role {
|
|
Role::GenericContainer
|
|
}
|
|
|
|
fn accessibility(&mut self, ctx: &mut AccessCtx) {
|
|
// TODO - Double check this code
|
|
// Not sure about these values
|
|
if false {
|
|
ctx.current_node().set_scroll_x(self.viewport_pos.x);
|
|
ctx.current_node().set_scroll_y(self.viewport_pos.y);
|
|
ctx.current_node().set_scroll_x_min(0.0);
|
|
ctx.current_node()
|
|
.set_scroll_x_max(self.scrollbar_horizontal.widget().portal_size);
|
|
ctx.current_node().set_scroll_y_min(0.0);
|
|
ctx.current_node()
|
|
.set_scroll_y_max(self.scrollbar_vertical.widget().portal_size);
|
|
}
|
|
|
|
ctx.current_node().set_clips_children();
|
|
|
|
self.child.accessibility(ctx);
|
|
self.scrollbar_horizontal.accessibility(ctx);
|
|
self.scrollbar_vertical.accessibility(ctx);
|
|
}
|
|
|
|
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
|
|
smallvec![self.child.as_dyn()]
|
|
}
|
|
|
|
fn make_trace_span(&self) -> Span {
|
|
trace_span!("Portal")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_debug_snapshot;
|
|
|
|
use super::*;
|
|
use crate::assert_render_snapshot;
|
|
use crate::testing::{widget_ids, TestHarness};
|
|
use crate::widget::{Button, Flex, SizedBox};
|
|
|
|
fn button(text: &'static str) -> impl Widget {
|
|
SizedBox::new(Button::new(text)).width(70.0).height(40.0)
|
|
}
|
|
|
|
// TODO - This test takes too long right now
|
|
#[test]
|
|
#[ignore]
|
|
fn button_list() {
|
|
let [item_3_id, item_13_id] = widget_ids();
|
|
|
|
let widget = Portal::new(
|
|
Flex::column()
|
|
.with_child(button("Item 1"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 2"))
|
|
.with_spacer(10.0)
|
|
.with_child_id(button("Item 3"), item_3_id)
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 4"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 5"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 6"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 7"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 8"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 9"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 10"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 11"))
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 12"))
|
|
.with_spacer(10.0)
|
|
.with_child_id(button("Item 13"), item_13_id)
|
|
.with_spacer(10.0)
|
|
.with_child(button("Item 14"))
|
|
.with_spacer(10.0),
|
|
);
|
|
|
|
let mut harness = TestHarness::create_with_size(widget, Size::new(400., 400.));
|
|
|
|
assert_debug_snapshot!(harness.root_widget());
|
|
assert_render_snapshot!(harness, "button_list_no_scroll");
|
|
|
|
harness.edit_root_widget(|mut portal| {
|
|
let mut portal = portal.downcast::<Portal<Flex>>();
|
|
portal.set_viewport_pos(Point::new(0.0, 130.0))
|
|
});
|
|
|
|
assert_render_snapshot!(harness, "button_list_scrolled");
|
|
|
|
let item_3_rect = harness.get_widget(item_3_id).state().layout_rect();
|
|
harness.edit_root_widget(|mut portal| {
|
|
let mut portal = portal.downcast::<Portal<Flex>>();
|
|
portal.pan_viewport_to(item_3_rect);
|
|
});
|
|
|
|
assert_render_snapshot!(harness, "button_list_scroll_to_item_3");
|
|
|
|
let item_13_rect = harness.get_widget(item_13_id).state().layout_rect();
|
|
harness.edit_root_widget(|mut portal| {
|
|
let mut portal = portal.downcast::<Portal<Flex>>();
|
|
portal.pan_viewport_to(item_13_rect);
|
|
});
|
|
|
|
assert_render_snapshot!(harness, "button_list_scroll_to_item_13");
|
|
}
|
|
|
|
// Helper function for panning tests
|
|
fn make_range(repr: &str) -> Range<f64> {
|
|
let repr = &repr[repr.find('_').unwrap()..];
|
|
|
|
let start = repr.find('x').unwrap();
|
|
let end = repr[start..].find('_').unwrap() + start;
|
|
|
|
assert!(repr[end..].chars().all(|c| c == '_'));
|
|
|
|
(start as f64)..(end as f64)
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_same() {
|
|
let initial_range = make_range("_______xxxx_____");
|
|
let target_range = make_range(" _______xxxx_____");
|
|
let result_range = make_range(" _______xxxx_____");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_smaller() {
|
|
let initial_range = make_range("_____xxxxxxxx___");
|
|
let target_range = make_range(" _______xxxx_____");
|
|
let result_range = make_range(" _____xxxxxxxx___");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_larger() {
|
|
let initial_range = make_range("_______xxxx_____");
|
|
let target_range = make_range(" _____xxxxxxxx___");
|
|
let result_range = make_range(" _______xxxx_____");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_left() {
|
|
let initial_range = make_range("_______xxxx_____");
|
|
let target_range = make_range(" ____xx__________");
|
|
let result_range = make_range(" ____xxxx________");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_left_intersects() {
|
|
let initial_range = make_range("_______xxxxx____");
|
|
let target_range = make_range(" ____xxxx________");
|
|
let result_range = make_range(" ____xxxxx_______");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_left_larger() {
|
|
let initial_range = make_range("__________xx____");
|
|
let target_range = make_range(" ____xxxx________");
|
|
let result_range = make_range(" ______xx________");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_left_larger_intersects() {
|
|
let initial_range = make_range("_______xx_______");
|
|
let target_range = make_range(" ____xxxx________");
|
|
let result_range = make_range(" ______xx________");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_right() {
|
|
let initial_range = make_range("_____xxxx_______");
|
|
let target_range = make_range(" __________xx____");
|
|
let result_range = make_range(" ________xxxx____");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_right_intersects() {
|
|
let initial_range = make_range("____xxxxx_______");
|
|
let target_range = make_range(" ________xxxx____");
|
|
let result_range = make_range(" _______xxxxx____");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_right_larger() {
|
|
let initial_range = make_range("____xx__________");
|
|
let target_range = make_range(" ________xxxx____");
|
|
let result_range = make_range(" ________xx______");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pan_to_right_larger_intersects() {
|
|
let initial_range = make_range("_______xx_______");
|
|
let target_range = make_range(" ________xxxx____");
|
|
let result_range = make_range(" ________xx______");
|
|
|
|
assert_eq!(compute_pan_range(initial_range, target_range), result_range);
|
|
}
|
|
}
|