Commit abe5e42a95d6
tarball4 files changed+260-16
@@ -593,6 +593,27 @@ impl Layout {
593593
done594594
}595595
596
+ /// Whether `w` is tiled with a visible window across the split on side `dir`.597
+ pub fn has_split(&self, w: WindowId, dir: Dir) -> bool {598
+ let Some(o) = self.windows.get(w).and_then(|w| w.output) else {599
+ return false;600
+ };601
+ self.outputs[o].tree.has_split(w, dir, self.vis_on(o))602
+ }603
+604
+ /// Moves the split on side `dir` of the tiled window `w` by `px`, positive605
+ /// toward the right or bottom.606
+ pub fn move_split(&mut self, w: WindowId, dir: Dir, px: f64) -> bool {607
+ let Some(o) = self.windows.get(w).and_then(|w| w.output) else {608
+ return false;609
+ };610
+ let area = inset(self.outputs[o].usable(), self.gaps.outer);611
+ let mut tree = std::mem::take(&mut self.outputs[o].tree);612
+ let moved = tree.move_split(w, dir, px, area, self.gaps.inner, self.vis_on(o));613
+ self.outputs[o].tree = tree;614
+ moved615
+ }616
+596617
pub fn expand(&mut self, scope: ExpandScope) -> bool {597618
let Some((w, o)) = self.focused_tiled() else {598619
return false;@@ -677,3 +677,39 @@ fn floats_shrink_into_a_smaller_output_they_are_evicted_to() {
677677
assert!(r.x >= usable.x && r.right() <= usable.right());678678
assert!(r.y >= usable.y && r.bottom() <= usable.bottom());679679
}680
+681
+/// Dragging a split moves only the two tiles that meet at it, whichever of682
+/// them the drag started from, and stops where one would vanish.683
+#[test]684
+fn dragging_a_split_moves_just_that_split() {685
+ let mut l = Layout::default();686
+ let o = l.add_output("a", AREA);687
+ let a = l.add_window(Some(o), None, None);688
+ let b = l.add_window(Some(o), None, None);689
+ let c = l.add_window(Some(o), None, None);690
+ let rect = |l: &Layout, w| {691
+ l.arrange(o)692
+ .tiled693
+ .iter()694
+ .find(|p| p.window == w)695
+ .unwrap()696
+ .rect697
+ };698
+ let before = [rect(&l, a), rect(&l, b), rect(&l, c)];699
+ assert!(!l.has_split(a, Dir::Up));700
+ assert!(!l.move_split(a, Dir::Up, 50.0));701
+ assert!(l.has_split(a, Dir::Down));702
+703
+ assert!(l.move_split(a, Dir::Down, 40.0));704
+ let (ra, rb) = (rect(&l, a), rect(&l, b));705
+ assert!((ra.h - before[0].h - 40.0).abs() < 1e-6);706
+ assert!((rb.h - before[1].h + 40.0).abs() < 1e-6);707
+ assert_eq!(rect(&l, c).h, before[2].h);708
+709
+ assert!(l.move_split(b, Dir::Up, -40.0));710
+ assert!((rect(&l, a).h - before[0].h).abs() < 1e-6);711
+712
+ l.move_split(a, Dir::Down, 1e6);713
+ assert!(rect(&l, b).h > 1.0);714
+ l.check();715
+}@@ -426,6 +426,100 @@ impl<W: Copy + Eq + Hash> Tree<W> {
426426
false427427
}428428
429
+ /// The lane a window's split on side `dir` lives in, as the child holding430
+ /// the window, its parent and the index of the visible sibling across431
+ /// the split.432
+ fn split_across(433
+ &self,434
+ w: W,435
+ dir: Dir,436
+ vis: &impl Fn(W) -> bool,437
+ ) -> Option<(NodeId, NodeId, usize)> {438
+ let mut cur = *self.leaves.get(&w)?;439
+ if !self.visible(cur, vis) {440
+ return None;441
+ }442
+ while let Some((p, i)) = self.position(cur) {443
+ if self.axis(p) == Some(dir.axis())444
+ && let Some(j) = self.next_shown(p, i, dir, vis)445
+ {446
+ return Some((cur, p, j));447
+ }448
+ cur = p;449
+ }450
+ None451
+ }452
+453
+ /// Whether the split on side `dir` of `w` has a visible window across it.454
+ pub fn has_split(&self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {455
+ self.split_across(w, dir, &vis).is_some()456
+ }457
+458
+ /// Moves the split on side `dir` of `w` by `px` along that side's axis,459
+ /// positive toward the right or bottom. Only the two children meeting at460
+ /// the split change, so the split follows the pointer.461
+ pub fn move_split(462
+ &mut self,463
+ w: W,464
+ dir: Dir,465
+ px: f64,466
+ area: Rect,467
+ gap: f64,468
+ vis: impl Fn(W) -> bool,469
+ ) -> bool {470
+ let Some((cur, p, j)) = self.split_across(w, dir, &vis) else {471
+ return false;472
+ };473
+ let axis = dir.axis();474
+ let other = self.children(p)[j];475
+ let (first, second) = if dir.forward() {476
+ (cur, other)477
+ } else {478
+ (other, cur)479
+ };480
+ let mut leaves = Vec::new();481
+ self.shown_leaves(first, &vis, &mut leaves);482
+ let (start, end) = self483
+ .arrange(area, gap, &vis)484
+ .iter()485
+ .filter(|(l, _)| leaves.contains(l))486
+ .map(|(_, r)| span(*r, axis))487
+ .fold((f64::MAX, f64::MIN), |(s, e), (rs, rl)| {488
+ (s.min(rs), e.max(rs + rl))489
+ });490
+ let ef = self.extent(first, axis, &vis);491
+ let es = self.extent(second, axis, &vis);492
+ if end <= start || ef <= 0.0 || es <= 0.0 {493
+ return true;494
+ }495
+ let sum: f64 = self496
+ .children(p)497
+ .iter()498
+ .filter(|&&c| self.visible(c, &vis))499
+ .map(|&c| self.extent(c, axis, &vis))500
+ .sum();501
+ let min = MIN_SHARE * sum;502
+ let du = (px * ef / (end - start)).clamp(-(ef - min).max(0.0), (es - min).max(0.0));503
+ self.scale(first, axis, (ef + du) / ef);504
+ self.scale(second, axis, (es - du) / es);505
+ true506
+ }507
+508
+ fn shown_leaves(&self, n: NodeId, vis: &impl Fn(W) -> bool, out: &mut Vec<W>) {509
+ match &self.nodes[n].kind {510
+ Kind::Leaf(w, _) => {511
+ if vis(*w) {512
+ out.push(*w);513
+ }514
+ }515
+ Kind::Container { children, .. } => {516
+ for &c in children {517
+ self.shown_leaves(c, vis, out);518
+ }519
+ }520
+ }521
+ }522
+429523
/// Scales every window under `n` along `axis`, hidden ones too, so they430524
/// keep their proportions for when they are shown again.431525
fn scale(&mut self, n: NodeId, axis: Axis, by: f64) {@@ -2,7 +2,7 @@
22
//! into these calls, with pointer positions in global logical coordinates.33
44
use crate::state::State;5
-use monolith_layout::{MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};5
+use monolith_layout::{Axis, Dir, Layout, MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};66
use smithay::backend::input::{ButtonState, InputTime, KeyState};77
use smithay::backend::session::Session;88
use smithay::input::keyboard::{FilterResult, Keycode, xkb};@@ -246,7 +246,7 @@ const BTN_LEFT: u32 = 0x110;
246246
const BTN_RIGHT: u32 = 0x111;247247
248248
/// The sides of a window a resize moves.249
-#[derive(Clone, Copy, Default)]249
+#[derive(Clone, Copy, Default, PartialEq, Eq)]250250
pub struct Edges {251251
left: bool,252252
right: bool,@@ -255,12 +255,52 @@ pub struct Edges {
255255
}256256
257257
impl Edges {258
- const BOTTOM_RIGHT: Self = Self {259
- left: false,260
- right: true,261
- top: false,262
- bottom: true,263
- };258
+ /// The edges a drag from pos resizes, from the cell of a 3x3 grid over259
+ /// the rect. The centre cell takes the nearest corner.260
+ fn grid(r: Rect, pos: Point<f64, Logical>) -> Self {261
+ let (fx, fy) = ((pos.x - r.x) / r.w, (pos.y - r.y) / r.h);262
+ let third = |f: f64| match f {263
+ f if f < 1.0 / 3.0 => -1,264
+ f if f > 2.0 / 3.0 => 1,265
+ _ => 0,266
+ };267
+ let (col, row) = match (third(fx), third(fy)) {268
+ (0, 0) => (if fx < 0.5 { -1 } else { 1 }, if fy < 0.5 { -1 } else { 1 }),269
+ cell => cell,270
+ };271
+ Self {272
+ left: col < 0,273
+ right: col > 0,274
+ top: row < 0,275
+ bottom: row > 0,276
+ }277
+ }278
+279
+ fn dirs(self) -> impl Iterator<Item = Dir> {280
+ [281
+ (Dir::Left, self.left),282
+ (Dir::Right, self.right),283
+ (Dir::Up, self.top),284
+ (Dir::Down, self.bottom),285
+ ]286
+ .into_iter()287
+ .filter_map(|(dir, on)| on.then_some(dir))288
+ }289
+290
+ /// Drops the edges of a tiled window that have no split to move.291
+ fn movable(self, layout: &Layout, id: WindowId) -> Self {292
+ let has = |dir, on: bool| on && layout.has_split(id, dir);293
+ Self {294
+ left: has(Dir::Left, self.left),295
+ right: has(Dir::Right, self.right),296
+ top: has(Dir::Up, self.top),297
+ bottom: has(Dir::Down, self.bottom),298
+ }299
+ }300
+301
+ fn any(self) -> bool {302
+ self.left || self.right || self.top || self.bottom303
+ }264304
265305
pub fn from_xdg(edge: xdg_toplevel::ResizeEdge) -> Option<Self> {266306
use xdg_toplevel::ResizeEdge as E;@@ -310,6 +350,8 @@ pub struct Drag {
310350
from: Point<f64, Logical>,311351
rect: Rect,312352
bounds: Rect,353
+ /// A tiled window, whose splits follow the pointer instead of a rect.354
+ tiled: bool,313355
/// None moves the window.314356
edges: Option<Edges>,315357
/// The client's button, whose release is still the client's to see.@@ -332,32 +374,67 @@ fn start_drag(state: &mut State, button: u32) -> bool {
332374
if !held {333375
return false;334376
}335
- let edges = (button == BTN_RIGHT).then_some(Edges::BOTTOM_RIGHT);377
+ let edges = match button {378
+ BTN_RIGHT => {379
+ let Some(rect) = window_rect(&state.wm.layout, id) else {380
+ return false;381
+ };382
+ Some(Edges::grid(rect, state.input.pointer_pos))383
+ }384
+ _ => None,385
+ };336386
begin_drag(state, id, edges, None)337387
}338388
389
+fn window_rect(layout: &Layout, id: WindowId) -> Option<Rect> {390
+ let win = layout.window(id)?;391
+ if win.floating {392
+ return win.float_rect;393
+ }394
+ layout395
+ .arrange(win.output?)396
+ .tiled397
+ .iter()398
+ .find(|p| p.window == id && p.visible)399
+ .map(|p| p.rect)400
+}401
+339402
impl State {340403
/// Starts moving, or with `edges` resizing, a floating window at a341404
/// client's request. Tiled windows are left alone.342405
pub fn client_drag(&mut self, id: WindowId, edges: Option<Edges>, button: u32) {343
- begin_drag(self, id, edges, Some(button));406
+ if self.wm.layout.window(id).is_some_and(|w| w.floating) {407
+ begin_drag(self, id, edges, Some(button));408
+ }344409
}345410
}346411
347412
fn begin_drag(state: &mut State, id: WindowId, edges: Option<Edges>, button: Option<u32>) -> bool {348413
let layout = &state.wm.layout;349
- let Some((rect, bounds)) = layout350
- .window(id)351
- .filter(|w| w.floating && !w.fullscreen)352
- .and_then(|w| Some((w.float_rect?, layout.output(w.output?)?.usable())))353
- else {414
+ let Some(win) = layout.window(id).filter(|w| !w.fullscreen) else {415
+ return false;416
+ };417
+ let tiled = !win.floating;418
+ let edges = if tiled {419
+ edges.map(|e| e.movable(layout, id)).filter(|e| e.any())420
+ } else {421
+ edges422
+ };423
+ if tiled && edges.is_none() {424
+ return false;425
+ }426
+ let (Some(rect), Some(out)) = (427
+ window_rect(layout, id),428
+ win.output.and_then(|o| layout.output(o)),429
+ ) else {354430
return false;355431
};356432
state.input.drag = Some(Drag {357433
window: id,358434
from: state.input.pointer_pos,359435
rect,360
- bounds,436
+ bounds: out.usable(),437
+ tiled,361438
edges,362439
button,363440
});@@ -392,6 +469,22 @@ fn drag_to(state: &mut State, pos: Point<f64, Logical>) -> bool {
392469
return false;393470
};394471
let (dx, dy) = (pos.x - d.from.x, pos.y - d.from.y);472
+ if let (true, Some(e)) = (d.tiled, d.edges) {473
+ let id = d.window;474
+ for dir in e.dirs() {475
+ let px = if dir.axis() == Axis::Horizontal {476
+ dx477
+ } else {478
+ dy479
+ };480
+ state.wm.layout.move_split(id, dir, px);481
+ }482
+ if let Some(d) = state.input.drag.as_mut() {483
+ d.from = pos;484
+ }485
+ state.apply_layout();486
+ return true;487
+ }395488
let r = d.rect;396489
let rect = match d.edges {397490
Some(e) => {