feat: add smart tiling navigation
This commit is contained in:
1 parent
58d91363d6
commit
8c1184ddda
3 files changed
+367
-22
No files matched your search
+40
-5
@@ -1,10 +1,10 @@
|
|||||||
use smithay::{
|
use smithay::{
|
||||||
backend::input::{
|
backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
||||||
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
||||||
},
|
},
|
||||||
input::{
|
input::{
|
||||||
keyboard::FilterResult,
|
keyboard::{FilterResult, keysyms},
|
||||||
pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerHandle, RelativeMotionEvent},
|
pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerHandle, RelativeMotionEvent},
|
||||||
},
|
},
|
||||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
@@ -13,19 +13,54 @@ use smithay::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
|
use ferese_layout::Direction;
|
||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
|
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
|
||||||
match event {
|
match event {
|
||||||
InputEvent::Keyboard { event, .. } => {
|
InputEvent::Keyboard { event, .. } => {
|
||||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||||
|
let keycode = event.key_code();
|
||||||
|
let state = event.state();
|
||||||
|
|
||||||
keyboard.input::<(), _>(
|
keyboard.input::<(), _>(
|
||||||
self,
|
self,
|
||||||
event.key_code(),
|
keycode,
|
||||||
event.state(),
|
state,
|
||||||
SERIAL_COUNTER.next_serial(),
|
SERIAL_COUNTER.next_serial(),
|
||||||
Event::time(&event) as u32,
|
Event::time(&event) as u32,
|
||||||
|_, _, _| FilterResult::Forward,
|
|data, modifiers, keysym| {
|
||||||
|
if state == KeyState::Released && data.intercepted_keys.remove(&keycode) {
|
||||||
|
return FilterResult::Intercept(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction = if modifiers.logo
|
||||||
|
&& !modifiers.ctrl
|
||||||
|
&& !modifiers.alt
|
||||||
|
&& !modifiers.shift
|
||||||
|
{
|
||||||
|
match keysym.modified_sym().raw() {
|
||||||
|
keysyms::KEY_h | keysyms::KEY_H => Some(Direction::Left),
|
||||||
|
keysyms::KEY_j | keysyms::KEY_J => Some(Direction::Down),
|
||||||
|
keysyms::KEY_k | keysyms::KEY_K => Some(Direction::Up),
|
||||||
|
keysyms::KEY_l | keysyms::KEY_L => Some(Direction::Right),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(direction) = direction {
|
||||||
|
if state == KeyState::Pressed {
|
||||||
|
data.intercepted_keys.insert(keycode);
|
||||||
|
data.focus_direction(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterResult::Intercept(())
|
||||||
|
} else {
|
||||||
|
FilterResult::Forward
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
InputEvent::PointerMotionAbsolute { event, .. } => {
|
InputEvent::PointerMotionAbsolute { event, .. } => {
|
||||||
|
|||||||
+72
-17
@@ -1,6 +1,12 @@
|
|||||||
use std::{collections::HashMap, error::Error, ffi::OsString, sync::Arc, time::Instant};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
error::Error,
|
||||||
|
ffi::OsString,
|
||||||
|
sync::Arc,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
use ferese_layout::{Axis, LayoutTree, Rect, WindowId};
|
use ferese_layout::{Axis, Direction, GapConfig, LayoutTree, Rect, WindowId};
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::{PopupManager, Space, Window, WindowSurfaceType},
|
desktop::{PopupManager, Space, Window, WindowSurfaceType},
|
||||||
@@ -40,6 +46,7 @@ pub struct Ferese {
|
|||||||
pub layout: LayoutTree,
|
pub layout: LayoutTree,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub window_ids: HashMap<Window, WindowId>,
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
|
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
|
||||||
next_window_id: u64,
|
next_window_id: u64,
|
||||||
pub popups: PopupManager,
|
pub popups: PopupManager,
|
||||||
pub seat: Seat<Self>,
|
pub seat: Seat<Self>,
|
||||||
@@ -94,6 +101,7 @@ impl Ferese {
|
|||||||
layout: LayoutTree::default(),
|
layout: LayoutTree::default(),
|
||||||
window_ids: HashMap::new(),
|
window_ids: HashMap::new(),
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
|
intercepted_keys: HashSet::new(),
|
||||||
next_window_id: 1,
|
next_window_id: 1,
|
||||||
popups: PopupManager::default(),
|
popups: PopupManager::default(),
|
||||||
seat,
|
seat,
|
||||||
@@ -160,10 +168,12 @@ impl Ferese {
|
|||||||
let id = WindowId(self.next_window_id);
|
let id = WindowId(self.next_window_id);
|
||||||
self.next_window_id += 1;
|
self.next_window_id += 1;
|
||||||
|
|
||||||
if let Err(error) = self
|
let axis = self
|
||||||
.layout
|
.output_bounds()
|
||||||
.insert(id, self.focused_window, Axis::Horizontal, 0.5)
|
.and_then(|bounds| self.layout.automatic_axis(self.focused_window, bounds).ok())
|
||||||
{
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
|
if let Err(error) = self.layout.insert(id, self.focused_window, axis, 0.5) {
|
||||||
tracing::error!(%error, ?id, "failed to insert window into layout");
|
tracing::error!(%error, ?id, "failed to insert window into layout");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -195,19 +205,10 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn relayout(&mut self) {
|
pub fn relayout(&mut self) {
|
||||||
let Some(output) = self.space.outputs().next() else {
|
let Some(bounds) = self.output_bounds() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(output_geometry) = self.space.output_geometry(output) else {
|
let geometry = match self.layout.geometry_with_gaps(bounds, GapConfig::default()) {
|
||||||
return;
|
|
||||||
};
|
|
||||||
let bounds = Rect::new(
|
|
||||||
output_geometry.loc.x as f64,
|
|
||||||
output_geometry.loc.y as f64,
|
|
||||||
output_geometry.size.w as f64,
|
|
||||||
output_geometry.size.h as f64,
|
|
||||||
);
|
|
||||||
let geometry = match self.layout.geometry(bounds) {
|
|
||||||
Ok(geometry) => geometry,
|
Ok(geometry) => geometry,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(%error, "failed to compute tiled geometry");
|
tracing::error!(%error, "failed to compute tiled geometry");
|
||||||
@@ -234,6 +235,60 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focus_direction(&mut self, direction: Direction) {
|
||||||
|
let Some(current) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(Some(next)) = self.layout.directional_neighbor(current, direction, bounds) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(window) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(surface) = window
|
||||||
|
.toplevel()
|
||||||
|
.map(|toplevel| toplevel.wl_surface().clone())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.focused_window = Some(next);
|
||||||
|
self.space.raise_element(&window, true);
|
||||||
|
self.seat
|
||||||
|
.get_keyboard()
|
||||||
|
.expect("seat has a keyboard")
|
||||||
|
.set_focus(
|
||||||
|
self,
|
||||||
|
Some(surface),
|
||||||
|
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for window in self.space.elements() {
|
||||||
|
if let Some(toplevel) = window.toplevel() {
|
||||||
|
toplevel.send_pending_configure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_bounds(&self) -> Option<Rect> {
|
||||||
|
let output = self.space.outputs().next()?;
|
||||||
|
let geometry = self.space.output_geometry(output)?;
|
||||||
|
|
||||||
|
Some(Rect::new(
|
||||||
|
geometry.loc.x as f64,
|
||||||
|
geometry.loc.y as f64,
|
||||||
|
geometry.size.w as f64,
|
||||||
|
geometry.size.h as f64,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ pub enum Axis {
|
|||||||
Vertical,
|
Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Direction {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Node {
|
pub enum Node {
|
||||||
Window(WindowId),
|
Window(WindowId),
|
||||||
@@ -49,6 +57,27 @@ impl Rect {
|
|||||||
height,
|
height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn center(self) -> (f64, f64) {
|
||||||
|
(self.x + self.width / 2.0, self.y + self.height / 2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct GapConfig {
|
||||||
|
pub inner: f64,
|
||||||
|
pub outer: f64,
|
||||||
|
pub smart: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GapConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: 10.0,
|
||||||
|
outer: 10.0,
|
||||||
|
smart: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -185,6 +214,134 @@ impl LayoutTree {
|
|||||||
Ok(geometry)
|
Ok(geometry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn geometry_with_gaps(
|
||||||
|
&self,
|
||||||
|
bounds: Rect,
|
||||||
|
gaps: GapConfig,
|
||||||
|
) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||||
|
let mut geometry = self.geometry(bounds)?;
|
||||||
|
let outer = if gaps.smart && geometry.len() == 1 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
valid_gap(gaps.outer)
|
||||||
|
};
|
||||||
|
let half_inner = valid_gap(gaps.inner) / 2.0;
|
||||||
|
let right = bounds.x + bounds.width;
|
||||||
|
let bottom = bounds.y + bounds.height;
|
||||||
|
|
||||||
|
for rect in geometry.values_mut() {
|
||||||
|
let left_inset = if nearly_equal(rect.x, bounds.x) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
let top_inset = if nearly_equal(rect.y, bounds.y) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
let right_inset = if nearly_equal(rect.x + rect.width, right) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
let bottom_inset = if nearly_equal(rect.y + rect.height, bottom) {
|
||||||
|
outer
|
||||||
|
} else {
|
||||||
|
half_inner
|
||||||
|
};
|
||||||
|
|
||||||
|
rect.x += left_inset;
|
||||||
|
rect.y += top_inset;
|
||||||
|
rect.width = (rect.width - left_inset - right_inset).max(1.0);
|
||||||
|
rect.height = (rect.height - top_inset - bottom_inset).max(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(geometry)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn automatic_axis(
|
||||||
|
&self,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<Axis, LayoutError> {
|
||||||
|
let Some(root) = self.root else {
|
||||||
|
return Ok(Axis::Horizontal);
|
||||||
|
};
|
||||||
|
let target = match focused {
|
||||||
|
Some(window) => window,
|
||||||
|
None => match self.nodes.get(&self.first_window(root)?) {
|
||||||
|
Some(Node::Window(window)) => *window,
|
||||||
|
_ => return Err(LayoutError::InvalidTree("leaf is not a window")),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let rect = self
|
||||||
|
.geometry(bounds)?
|
||||||
|
.remove(&target)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(target))?;
|
||||||
|
|
||||||
|
Ok(if rect.width > rect.height {
|
||||||
|
Axis::Horizontal
|
||||||
|
} else {
|
||||||
|
Axis::Vertical
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn directional_neighbor(
|
||||||
|
&self,
|
||||||
|
window: WindowId,
|
||||||
|
direction: Direction,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> Result<Option<WindowId>, LayoutError> {
|
||||||
|
let geometry = self.geometry(bounds)?;
|
||||||
|
let target = geometry
|
||||||
|
.get(&window)
|
||||||
|
.copied()
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
let (target_x, target_y) = target.center();
|
||||||
|
|
||||||
|
Ok(geometry
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(candidate, _)| *candidate != window)
|
||||||
|
.filter_map(|(candidate, rect)| {
|
||||||
|
let (candidate_x, candidate_y) = rect.center();
|
||||||
|
let (primary, secondary, aligned) = match direction {
|
||||||
|
Direction::Left if candidate_x < target_x => (
|
||||||
|
target_x - candidate_x,
|
||||||
|
(target_y - candidate_y).abs(),
|
||||||
|
intervals_overlap(target.y, target.height, rect.y, rect.height),
|
||||||
|
),
|
||||||
|
Direction::Right if candidate_x > target_x => (
|
||||||
|
candidate_x - target_x,
|
||||||
|
(target_y - candidate_y).abs(),
|
||||||
|
intervals_overlap(target.y, target.height, rect.y, rect.height),
|
||||||
|
),
|
||||||
|
Direction::Up if candidate_y < target_y => (
|
||||||
|
target_y - candidate_y,
|
||||||
|
(target_x - candidate_x).abs(),
|
||||||
|
intervals_overlap(target.x, target.width, rect.x, rect.width),
|
||||||
|
),
|
||||||
|
Direction::Down if candidate_y > target_y => (
|
||||||
|
candidate_y - target_y,
|
||||||
|
(target_x - candidate_x).abs(),
|
||||||
|
intervals_overlap(target.x, target.width, rect.x, rect.width),
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((candidate, primary, secondary, aligned))
|
||||||
|
})
|
||||||
|
.min_by(|left, right| {
|
||||||
|
right
|
||||||
|
.3
|
||||||
|
.cmp(&left.3)
|
||||||
|
.then_with(|| left.1.total_cmp(&right.1))
|
||||||
|
.then_with(|| left.2.total_cmp(&right.2))
|
||||||
|
.then_with(|| left.0.0.cmp(&right.0.0))
|
||||||
|
})
|
||||||
|
.map(|(candidate, _, _, _)| candidate))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), LayoutError> {
|
pub fn validate(&self) -> Result<(), LayoutError> {
|
||||||
let Some(root) = self.root else {
|
let Some(root) = self.root else {
|
||||||
return if self.nodes.is_empty() && self.parents.is_empty() && self.windows.is_empty() {
|
return if self.nodes.is_empty() && self.parents.is_empty() && self.windows.is_empty() {
|
||||||
@@ -351,6 +508,23 @@ fn normalized_ratio(ratio: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn valid_gap(gap: f64) -> f64 {
|
||||||
|
if gap.is_finite() { gap.max(0.0) } else { 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nearly_equal(left: f64, right: f64) -> bool {
|
||||||
|
(left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intervals_overlap(
|
||||||
|
first_start: f64,
|
||||||
|
first_size: f64,
|
||||||
|
second_start: f64,
|
||||||
|
second_size: f64,
|
||||||
|
) -> bool {
|
||||||
|
first_start < second_start + second_size && second_start < first_start + first_size
|
||||||
|
}
|
||||||
|
|
||||||
fn split_rect(bounds: Rect, axis: Axis, ratio: f64) -> (Rect, Rect) {
|
fn split_rect(bounds: Rect, axis: Axis, ratio: f64) -> (Rect, Rect) {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::Horizontal => {
|
Axis::Horizontal => {
|
||||||
@@ -456,4 +630,85 @@ mod tests {
|
|||||||
assert_eq!(normalized_ratio(f64::NAN), 0.5);
|
assert_eq!(normalized_ratio(f64::NAN), 0.5);
|
||||||
assert_eq!(normalized_ratio(f64::INFINITY), 0.5);
|
assert_eq!(normalized_ratio(f64::INFINITY), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn automatic_axis_follows_focused_tile_shape() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let wide = Rect::new(0.0, 0.0, 1_920.0, 1_080.0);
|
||||||
|
assert_eq!(
|
||||||
|
tree.automatic_axis(Some(WindowId(1)), wide).unwrap(),
|
||||||
|
Axis::Horizontal
|
||||||
|
);
|
||||||
|
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
tree.automatic_axis(Some(WindowId(1)), wide).unwrap(),
|
||||||
|
Axis::Vertical
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gaps_preserve_outer_and_shared_spacing() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let geometry = tree
|
||||||
|
.geometry_with_gaps(Rect::new(0.0, 0.0, 100.0, 80.0), GapConfig::default())
|
||||||
|
.unwrap();
|
||||||
|
let first = geometry[&WindowId(1)];
|
||||||
|
let second = geometry[&WindowId(2)];
|
||||||
|
|
||||||
|
assert_eq!(first.x, 10.0);
|
||||||
|
assert_eq!(second.x + second.width, 90.0);
|
||||||
|
assert_eq!(second.x - (first.x + first.width), 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smart_gaps_remove_outer_gap_for_one_window() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
||||||
|
let geometry = tree
|
||||||
|
.geometry_with_gaps(bounds, GapConfig::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(geometry[&WindowId(1)], bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn directional_neighbor_prefers_primary_distance() {
|
||||||
|
let mut tree = LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(3), Some(WindowId(2)), Axis::Vertical, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||||
|
assert_eq!(
|
||||||
|
tree.directional_neighbor(WindowId(1), Direction::Right, bounds)
|
||||||
|
.unwrap(),
|
||||||
|
Some(WindowId(2))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tree.directional_neighbor(WindowId(2), Direction::Down, bounds)
|
||||||
|
.unwrap(),
|
||||||
|
Some(WindowId(3))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tree.directional_neighbor(WindowId(1), Direction::Left, bounds)
|
||||||
|
.unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user