feat: reveal overflow tabs while holding a native drag at strip edges
This commit is contained in:
+137
-4
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
num::NonZeroU32,
|
||||
sync::{Arc, RwLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use softbuffer::{Context, Surface};
|
||||
@@ -8,7 +9,7 @@ use winit::{
|
||||
application::ApplicationHandler,
|
||||
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
|
||||
event::{ElementState, Ime, MouseButton, MouseScrollDelta, WindowEvent},
|
||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy},
|
||||
keyboard::{Key, ModifiersState, NamedKey},
|
||||
window::{Window, WindowAttributes, WindowId},
|
||||
};
|
||||
@@ -29,6 +30,14 @@ pub enum AppEvent {
|
||||
ConsoleFailed { message: String, source: String },
|
||||
}
|
||||
|
||||
struct TabScrollHover {
|
||||
window: u64,
|
||||
session: u64,
|
||||
point: Point,
|
||||
direction: i8,
|
||||
next: Instant,
|
||||
}
|
||||
|
||||
pub struct TesseraApp {
|
||||
resource: u64,
|
||||
native_session: u64,
|
||||
@@ -50,6 +59,8 @@ pub struct TesseraApp {
|
||||
tab_drag_origin: Option<Point>,
|
||||
tab_drag_grab: Option<Point>,
|
||||
tab_drop_index: Option<usize>,
|
||||
tab_first: Option<usize>,
|
||||
tab_scroll_hover: Option<TabScrollHover>,
|
||||
window_order: Vec<u64>,
|
||||
guest_focus: Option<crate::events::Input>,
|
||||
guest_key: Option<(NamedKey, crate::events::Input)>,
|
||||
@@ -88,6 +99,8 @@ impl TesseraApp {
|
||||
tab_drag_origin: None,
|
||||
tab_drag_grab: None,
|
||||
tab_drop_index: None,
|
||||
tab_first: None,
|
||||
tab_scroll_hover: None,
|
||||
window_order: vec![crate::window::MAIN_WINDOW],
|
||||
guest_focus: None,
|
||||
guest_key: None,
|
||||
@@ -285,6 +298,8 @@ impl TesseraApp {
|
||||
self.tab_drag_origin = None;
|
||||
self.tab_drag_grab = None;
|
||||
self.tab_drop_index = None;
|
||||
self.tab_first = None;
|
||||
self.tab_scroll_hover = None;
|
||||
}
|
||||
self.guest_focus =
|
||||
frame
|
||||
@@ -486,7 +501,7 @@ impl TesseraApp {
|
||||
return None;
|
||||
}
|
||||
let width = window.inner_size().width as f32 / window.scale_factor() as f32;
|
||||
let (id, close) = crate::tabs::hit(&frame.tabs, width, self.pointer)?;
|
||||
let (id, close) = crate::tabs::hit(self.tab_first, &frame.tabs, width, self.pointer)?;
|
||||
Some(crate::events::Input {
|
||||
window: self.resource,
|
||||
session: frame.session,
|
||||
@@ -585,6 +600,7 @@ impl TesseraApp {
|
||||
let window = self.window.as_ref()?;
|
||||
let current = self.namespace.read().ok()?;
|
||||
crate::tabs::drag_anchor(
|
||||
self.tab_first,
|
||||
¤t.document.frame.tabs,
|
||||
window.inner_size().width as f32 / window.scale_factor() as f32,
|
||||
input.mount,
|
||||
@@ -605,6 +621,78 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_tab_scroll(&mut self) {
|
||||
self.tab_scroll_hover = None;
|
||||
if self.tab_first.take().is_some() {
|
||||
self.request_redraw();
|
||||
}
|
||||
for child in self.children.values_mut() {
|
||||
child.clear_tab_scroll();
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_tab_scroll(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let Some(mut hover) = self.tab_scroll_hover.take() else {
|
||||
return;
|
||||
};
|
||||
let dragging = self.apps().any(|app| {
|
||||
app.tab_drag_origin.is_some()
|
||||
&& app.tab_pressed.as_ref().is_some_and(|input| {
|
||||
input.session == hover.session
|
||||
&& app.namespace.read().ok().is_some_and(|current| {
|
||||
let frame = ¤t.document.frame;
|
||||
frame.session == input.session
|
||||
&& frame.fault.is_none()
|
||||
&& frame.tabs.iter().any(|tab| tab.id == input.mount)
|
||||
})
|
||||
})
|
||||
});
|
||||
if !dragging {
|
||||
self.clear_tab_scroll();
|
||||
return;
|
||||
}
|
||||
let now = Instant::now();
|
||||
if now < hover.next {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(hover.next));
|
||||
self.tab_scroll_hover = Some(hover);
|
||||
return;
|
||||
}
|
||||
let target = if self.resource == hover.window {
|
||||
Some(&mut *self)
|
||||
} else {
|
||||
self.children.get_mut(&hover.window)
|
||||
};
|
||||
let feedback = target.and_then(|target| {
|
||||
let window = target.window.as_ref()?;
|
||||
let width = window.inner_size().width as f32 / window.scale_factor() as f32;
|
||||
let current = target.namespace.read().ok()?;
|
||||
let frame = ¤t.document.frame;
|
||||
if frame.session != hover.session
|
||||
|| frame.fault.is_some()
|
||||
|| target.guest_covered(hover.point)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let direction =
|
||||
crate::tabs::edge_direction(&frame.tabs, width, target.tab_first, hover.point);
|
||||
if direction != hover.direction {
|
||||
return None;
|
||||
}
|
||||
let first = crate::tabs::scroll_start(&frame.tabs, width, target.tab_first, direction)?;
|
||||
let index = crate::tabs::insertion_index(Some(first), &frame.tabs, width, hover.point)?;
|
||||
drop(current);
|
||||
target.tab_first = Some(first);
|
||||
target.request_redraw();
|
||||
Some((hover.window, index))
|
||||
});
|
||||
if let Some(feedback) = feedback {
|
||||
self.clear_drag_feedback(Some(feedback));
|
||||
hover.next = now + Duration::from_millis(120);
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(hover.next));
|
||||
self.tab_scroll_hover = Some(hover);
|
||||
}
|
||||
}
|
||||
|
||||
fn native_tab_drag(&mut self, window_id: WindowId, event: &WindowEvent) {
|
||||
if self.resource != crate::window::MAIN_WINDOW {
|
||||
return;
|
||||
@@ -617,6 +705,17 @@ impl TesseraApp {
|
||||
return;
|
||||
};
|
||||
let receiver_id = receiver.resource;
|
||||
if matches!(event, WindowEvent::CursorLeft { .. }) {
|
||||
if self
|
||||
.tab_scroll_hover
|
||||
.as_ref()
|
||||
.is_some_and(|hover| hover.window == receiver_id)
|
||||
{
|
||||
self.tab_scroll_hover = None;
|
||||
self.clear_drag_feedback(None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if matches!(
|
||||
event,
|
||||
WindowEvent::Focused(true)
|
||||
@@ -647,6 +746,7 @@ impl TesseraApp {
|
||||
child.tab_drag_grab = None;
|
||||
}
|
||||
self.clear_drag_feedback(None);
|
||||
self.clear_tab_scroll();
|
||||
return;
|
||||
}
|
||||
let release = matches!(
|
||||
@@ -728,6 +828,7 @@ impl TesseraApp {
|
||||
child.tab_drag_grab = None;
|
||||
}
|
||||
self.clear_drag_feedback(None);
|
||||
self.clear_tab_scroll();
|
||||
return;
|
||||
}
|
||||
let mut apps: Vec<_> = self.apps().collect();
|
||||
@@ -771,6 +872,7 @@ impl TesseraApp {
|
||||
});
|
||||
// An obscuring window (including its chrome/developer panel) prevents
|
||||
// dropping into a lower window that happens to share these coordinates.
|
||||
let mut scroll = None;
|
||||
let target = front.and_then(|(app, relative, size, inside)| {
|
||||
if !inside || app.guest_covered(relative) {
|
||||
return None;
|
||||
@@ -783,8 +885,13 @@ impl TesseraApp {
|
||||
let index = if frame.tabs.is_empty() {
|
||||
0
|
||||
} else {
|
||||
crate::tabs::insertion_index(&frame.tabs, size.width, relative)?
|
||||
crate::tabs::insertion_index(app.tab_first, &frame.tabs, size.width, relative)?
|
||||
};
|
||||
let direction =
|
||||
crate::tabs::edge_direction(&frame.tabs, size.width, app.tab_first, relative);
|
||||
if direction != 0 {
|
||||
scroll = Some((app.resource, frame.session, relative, direction));
|
||||
}
|
||||
Some(crate::events::TabDrop {
|
||||
window: app.resource,
|
||||
layout_version: frame.layout_version,
|
||||
@@ -802,6 +909,23 @@ impl TesseraApp {
|
||||
.all(|value| value.is_finite() && value.abs() <= 1_000_000.0)
|
||||
.then_some([x.round() as i32, y.round() as i32])
|
||||
});
|
||||
self.tab_scroll_hover = scroll.map(|(window, session, point, direction)| {
|
||||
let next = self
|
||||
.tab_scroll_hover
|
||||
.as_ref()
|
||||
.filter(|old| {
|
||||
old.window == window && old.session == session && old.direction == direction
|
||||
})
|
||||
.map(|old| old.next)
|
||||
.unwrap_or_else(|| Instant::now() + Duration::from_millis(350));
|
||||
TabScrollHover {
|
||||
window,
|
||||
session,
|
||||
point,
|
||||
direction,
|
||||
next,
|
||||
}
|
||||
});
|
||||
self.clear_drag_feedback(target.as_ref().map(|target| (target.window, target.index)));
|
||||
if !release {
|
||||
return;
|
||||
@@ -829,6 +953,7 @@ impl TesseraApp {
|
||||
source.guest_pressed = None;
|
||||
source.send_guest(input);
|
||||
self.clear_drag_feedback(None);
|
||||
self.clear_tab_scroll();
|
||||
}
|
||||
|
||||
fn create_native(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
|
||||
@@ -914,7 +1039,10 @@ impl TesseraApp {
|
||||
&namespace.document,
|
||||
&mut self.developer_tools,
|
||||
&self.activity,
|
||||
self.tab_drop_index,
|
||||
crate::tabs::Chrome {
|
||||
first: self.tab_first,
|
||||
insertion: self.tab_drop_index,
|
||||
},
|
||||
);
|
||||
drop(namespace);
|
||||
self.layout = scene.layout;
|
||||
@@ -951,6 +1079,11 @@ impl TesseraApp {
|
||||
}
|
||||
|
||||
impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
self.tick_tab_scroll(event_loop);
|
||||
}
|
||||
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if let Err(error) = self.create_native(event_loop) {
|
||||
self.native_error = Some(format!(
|
||||
|
||||
+4
-2
@@ -17,7 +17,7 @@ pub fn build_scene(
|
||||
document: &RenderDocument,
|
||||
developer_tools_state: &mut DeveloperToolsState,
|
||||
activity: &Activity,
|
||||
tab_drop_index: Option<usize>,
|
||||
tab_chrome: crate::tabs::Chrome,
|
||||
) -> DisplayScene {
|
||||
let mut display_list = document.display_list(scale).unwrap_or_else(|_| {
|
||||
RenderDocument::default()
|
||||
@@ -27,13 +27,15 @@ pub fn build_scene(
|
||||
let offset = crate::tabs::height(&document.frame.tabs);
|
||||
display_list.translate(crate::geometry::Point::new(0.0, offset * scale));
|
||||
crate::tabs::draw(
|
||||
tab_chrome.first,
|
||||
&mut display_list,
|
||||
&document.frame.tabs,
|
||||
width as f32 / scale,
|
||||
scale,
|
||||
);
|
||||
if let Some(index) = tab_drop_index {
|
||||
if let Some(index) = tab_chrome.insertion {
|
||||
crate::tabs::draw_insertion(
|
||||
tab_chrome.first,
|
||||
&mut display_list,
|
||||
&document.frame.tabs,
|
||||
width as f32 / scale,
|
||||
|
||||
+175
-31
@@ -7,6 +7,11 @@ use crate::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const HEIGHT: f32 = 36.0;
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Chrome {
|
||||
pub first: Option<usize>,
|
||||
pub insertion: Option<usize>,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Tab {
|
||||
pub id: u64,
|
||||
@@ -18,24 +23,62 @@ pub fn height(tabs: &[Tab]) -> f32 {
|
||||
if tabs.is_empty() { 0.0 } else { HEIGHT }
|
||||
}
|
||||
|
||||
/// A drag viewport is presentation state only: it never activates a Page.
|
||||
fn visible_range(tabs: &[Tab], width: f32, start: Option<usize>) -> std::ops::Range<usize> {
|
||||
let count = (((width - 64.0) / 160.0).floor().max(1.0) as usize).min(tabs.len());
|
||||
if tabs.len() <= count {
|
||||
return 0..tabs.len();
|
||||
}
|
||||
let active = tabs.iter().position(|tab| tab.active).unwrap_or(0);
|
||||
let first = start
|
||||
.unwrap_or_else(|| active.saturating_sub(count / 2))
|
||||
.min(tabs.len() - count);
|
||||
first..first + count
|
||||
}
|
||||
|
||||
pub fn edge_direction(tabs: &[Tab], width: f32, start: Option<usize>, point: Point) -> i8 {
|
||||
if point.x < 0.0 || point.x >= width || point.y < 0.0 || point.y >= HEIGHT {
|
||||
return 0;
|
||||
}
|
||||
let visible = visible_range(tabs, width, start);
|
||||
if point.x < 32.0 && visible.start > 0 {
|
||||
-1
|
||||
} else if point.x >= width - 32.0 && visible.end < tabs.len() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_start(
|
||||
tabs: &[Tab],
|
||||
width: f32,
|
||||
start: Option<usize>,
|
||||
direction: i8,
|
||||
) -> Option<usize> {
|
||||
let visible = visible_range(tabs, width, start);
|
||||
if direction < 0 && visible.start > 0 {
|
||||
Some(visible.start - 1)
|
||||
} else if direction > 0 && visible.end < tabs.len() {
|
||||
Some(visible.start + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct Item<'a> {
|
||||
tab: &'a Tab,
|
||||
rect: Rect,
|
||||
navigation: Option<&'static str>,
|
||||
}
|
||||
fn items(tabs: &[Tab], width: f32) -> Vec<Item<'_>> {
|
||||
fn items(start: Option<usize>, tabs: &[Tab], width: f32) -> Vec<Item<'_>> {
|
||||
if tabs.is_empty() || width <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let capacity = ((width - 64.0) / 160.0).floor().max(1.0) as usize;
|
||||
let overflow = tabs.len() > capacity;
|
||||
let count = if overflow { capacity } else { tabs.len() };
|
||||
let active = tabs.iter().position(|tab| tab.active).unwrap_or(0);
|
||||
let start = if overflow {
|
||||
active.saturating_sub(count / 2).min(tabs.len() - count)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let visible = visible_range(tabs, width, start);
|
||||
let start = visible.start;
|
||||
let count = visible.len();
|
||||
let overflow = count < tabs.len();
|
||||
let margin = if overflow { 32.0 } else { 0.0 };
|
||||
let tab_width = ((width - margin * 2.0) / count as f32).min(220.0);
|
||||
let mut result = Vec::new();
|
||||
@@ -66,11 +109,11 @@ fn close_rect(rect: Rect) -> Rect {
|
||||
Rect::new(rect.x + rect.width - 27.0, 5.0, 24.0, 26.0)
|
||||
}
|
||||
|
||||
pub fn hit(tabs: &[Tab], width: f32, point: Point) -> Option<(u64, bool)> {
|
||||
pub fn hit(start: Option<usize>, tabs: &[Tab], width: f32, point: Point) -> Option<(u64, bool)> {
|
||||
if point.x < 0.0 || point.x >= width || point.y < 0.0 || point.y >= height(tabs) {
|
||||
return None;
|
||||
}
|
||||
items(tabs, width)
|
||||
items(start, tabs, width)
|
||||
.into_iter()
|
||||
.find(|item| item.rect.contains(point))
|
||||
.map(|item| {
|
||||
@@ -82,14 +125,19 @@ pub fn hit(tabs: &[Tab], width: f32, point: Point) -> Option<(u64, bool)> {
|
||||
}
|
||||
|
||||
/// Gap index in the complete published order, including overflow-hidden tabs.
|
||||
pub fn insertion_index(tabs: &[Tab], width: f32, point: Point) -> Option<usize> {
|
||||
pub fn insertion_index(
|
||||
start: Option<usize>,
|
||||
tabs: &[Tab],
|
||||
width: f32,
|
||||
point: Point,
|
||||
) -> Option<usize> {
|
||||
if point.x < 0.0 || point.x >= width || point.y < 0.0 || point.y >= HEIGHT {
|
||||
return None;
|
||||
}
|
||||
if tabs.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
let visible: Vec<_> = items(tabs, width)
|
||||
let visible: Vec<_> = items(start, tabs, width)
|
||||
.into_iter()
|
||||
.filter(|item| item.navigation.is_none())
|
||||
.collect();
|
||||
@@ -104,15 +152,28 @@ pub fn insertion_index(tabs: &[Tab], width: f32, point: Point) -> Option<usize>
|
||||
.map(|index| index + 1)
|
||||
}
|
||||
|
||||
pub fn drag_anchor(tabs: &[Tab], width: f32, id: u64, point: Point) -> Option<Point> {
|
||||
let item = items(tabs, width)
|
||||
pub fn drag_anchor(
|
||||
start: Option<usize>,
|
||||
tabs: &[Tab],
|
||||
width: f32,
|
||||
id: u64,
|
||||
point: Point,
|
||||
) -> Option<Point> {
|
||||
let item = items(start, tabs, width)
|
||||
.into_iter()
|
||||
.find(|item| item.tab.id == id && item.navigation.is_none() && item.rect.contains(point))?;
|
||||
(!close_rect(item.rect).contains(point)).then(|| Point::new(point.x - item.rect.x, point.y))
|
||||
}
|
||||
|
||||
pub fn draw_insertion(list: &mut DisplayList, tabs: &[Tab], width: f32, index: usize, scale: f32) {
|
||||
let visible: Vec<_> = items(tabs, width)
|
||||
pub fn draw_insertion(
|
||||
start: Option<usize>,
|
||||
list: &mut DisplayList,
|
||||
tabs: &[Tab],
|
||||
width: f32,
|
||||
index: usize,
|
||||
scale: f32,
|
||||
) {
|
||||
let visible: Vec<_> = items(start, tabs, width)
|
||||
.into_iter()
|
||||
.filter(|item| item.navigation.is_none())
|
||||
.collect();
|
||||
@@ -146,7 +207,7 @@ pub fn draw_insertion(list: &mut DisplayList, tabs: &[Tab], width: f32, index: u
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(list: &mut DisplayList, tabs: &[Tab], width: f32, scale: f32) {
|
||||
pub fn draw(start: Option<usize>, list: &mut DisplayList, tabs: &[Tab], width: f32, scale: f32) {
|
||||
if tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -163,7 +224,7 @@ pub fn draw(list: &mut DisplayList, tabs: &[Tab], width: f32, scale: f32) {
|
||||
radius: 0.0,
|
||||
color: Color::rgb(230, 232, 236),
|
||||
});
|
||||
for item in items(tabs, width) {
|
||||
for item in items(start, tabs, width) {
|
||||
if item.tab.active && item.navigation.is_none() {
|
||||
list.push(DrawCommand::FillRoundedRect {
|
||||
rect: scaled(item.rect),
|
||||
@@ -221,25 +282,28 @@ mod tests {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
insertion_index(&tabs, 1000.0, Point::new(10.0, 18.0)),
|
||||
insertion_index(None, &tabs, 1000.0, Point::new(10.0, 18.0)),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
insertion_index(&tabs, 1000.0, Point::new(215.0, 18.0)),
|
||||
insertion_index(None, &tabs, 1000.0, Point::new(215.0, 18.0)),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
insertion_index(&tabs, 1000.0, Point::new(990.0, 18.0)),
|
||||
insertion_index(None, &tabs, 1000.0, Point::new(990.0, 18.0)),
|
||||
Some(4)
|
||||
);
|
||||
assert!(drag_anchor(&tabs, 1000.0, 1, Point::new(200.0, 18.0)).is_none());
|
||||
assert!(drag_anchor(&tabs, 1000.0, 1, Point::new(25.0, 18.0)).is_some());
|
||||
assert!(drag_anchor(None, &tabs, 1000.0, 1, Point::new(200.0, 18.0)).is_none());
|
||||
assert!(drag_anchor(None, &tabs, 1000.0, 1, Point::new(25.0, 18.0)).is_some());
|
||||
assert_eq!(
|
||||
insertion_index(&tabs, 1000.0, Point::new(20.0, HEIGHT)),
|
||||
insertion_index(None, &tabs, 1000.0, Point::new(20.0, HEIGHT)),
|
||||
None
|
||||
);
|
||||
assert!(drag_anchor(&tabs, 400.0, 3, Point::new(390.0, 18.0)).is_none());
|
||||
assert_eq!(insertion_index(&[], 720.0, Point::new(20.0, 18.0)), Some(0));
|
||||
assert!(drag_anchor(None, &tabs, 400.0, 3, Point::new(390.0, 18.0)).is_none());
|
||||
assert_eq!(
|
||||
insertion_index(None, &[], 720.0, Point::new(20.0, 18.0)),
|
||||
Some(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -251,7 +315,7 @@ mod tests {
|
||||
active: id == 32,
|
||||
})
|
||||
.collect();
|
||||
let visible = items(&tabs, 720.0);
|
||||
let visible = items(None, &tabs, 720.0);
|
||||
assert!(
|
||||
visible
|
||||
.iter()
|
||||
@@ -259,17 +323,97 @@ mod tests {
|
||||
);
|
||||
for item in &visible {
|
||||
let point = Point::new(item.rect.x + 12.0, 18.0);
|
||||
assert_eq!(hit(&tabs, 720.0, point), Some((item.tab.id, false)));
|
||||
assert_eq!(hit(None, &tabs, 720.0, point), Some((item.tab.id, false)));
|
||||
}
|
||||
let tab = visible.iter().find(|item| item.tab.active).unwrap();
|
||||
assert_eq!(
|
||||
hit(
|
||||
None,
|
||||
&tabs,
|
||||
720.0,
|
||||
Point::new(tab.rect.x + tab.rect.width - 15.0, 18.0)
|
||||
),
|
||||
Some((32, true))
|
||||
);
|
||||
assert_eq!(hit(&tabs, 720.0, Point::new(50.0, HEIGHT)), None);
|
||||
assert_eq!(hit(None, &tabs, 720.0, Point::new(50.0, HEIGHT)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drag_viewport_reaches_hidden_ends_without_changing_active_page() {
|
||||
let tabs: Vec<_> = (1..=64)
|
||||
.map(|id| Tab {
|
||||
id,
|
||||
title: id.to_string(),
|
||||
active: id == 32,
|
||||
})
|
||||
.collect();
|
||||
let width = 720.0;
|
||||
let mut start = None;
|
||||
while let Some(next) = scroll_start(&tabs, width, start, 1) {
|
||||
start = Some(next);
|
||||
}
|
||||
assert_eq!(visible_range(&tabs, width, start), 60..64);
|
||||
assert_eq!(
|
||||
edge_direction(&tabs, width, start, Point::new(710.0, 18.0)),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
insertion_index(start, &tabs, width, Point::new(710.0, 18.0)),
|
||||
Some(64)
|
||||
);
|
||||
while let Some(next) = scroll_start(&tabs, width, start, -1) {
|
||||
start = Some(next);
|
||||
}
|
||||
assert_eq!(visible_range(&tabs, width, start), 0..4);
|
||||
assert_eq!(
|
||||
edge_direction(&tabs, width, start, Point::new(10.0, 18.0)),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
insertion_index(start, &tabs, width, Point::new(10.0, 18.0)),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
tabs.iter()
|
||||
.filter(|tab| tab.active)
|
||||
.map(|tab| tab.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![32]
|
||||
);
|
||||
assert!(
|
||||
items(None, &tabs, width)
|
||||
.iter()
|
||||
.any(|item| item.tab.active && item.navigation.is_none())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolled_hit_and_gap_geometry_survive_resize_and_list_shrink() {
|
||||
let tabs: Vec<_> = (1..=8)
|
||||
.map(|id| Tab {
|
||||
id,
|
||||
title: id.to_string(),
|
||||
active: id == 1,
|
||||
})
|
||||
.collect();
|
||||
let start = Some(4);
|
||||
assert_eq!(
|
||||
hit(start, &tabs, 720.0, Point::new(40.0, 18.0)),
|
||||
Some((5, false))
|
||||
);
|
||||
assert_eq!(
|
||||
insertion_index(start, &tabs, 720.0, Point::new(40.0, 18.0)),
|
||||
Some(4)
|
||||
);
|
||||
assert_eq!(visible_range(&tabs[..3], 720.0, start), 0..3);
|
||||
assert_eq!(visible_range(&tabs, 1920.0, start), 0..8);
|
||||
assert_eq!(
|
||||
edge_direction(&tabs, 720.0, start, Point::new(710.0, HEIGHT)),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
edge_direction(&tabs, 720.0, start, Point::new(725.0, 18.0)),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user