From 3b7ccb6f4a9cee6697c8eba809e84e815ed166d7 Mon Sep 17 00:00:00 2001 From: Vidar Hokstad Date: Sun, 14 Jan 2024 21:09:00 +0000 Subject: [PATCH] Support resize of tiling windows --- desktop.rb | 1 + geom.rb | 20 ++++++--- node.rb | 13 +++--- rubywm.rb | 67 +++--------------------------- wm.rb | 120 +++++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 142 insertions(+), 79 deletions(-) diff --git a/desktop.rb b/desktop.rb index d203910..732a75f 100644 --- a/desktop.rb +++ b/desktop.rb @@ -5,6 +5,7 @@ class Desktop def initialize(wm, id, name) = (@wm, @id, @name = wm, id, name) def active? = (@wm.current_desktop_id == self.id) def children = @wm.windows.values.find_all{_1.desktop==self} + def mapped_regular_children = children.find_all{_1.mapped && !_1.special?} def show = children.each(&:show) def hide = children.each(&:hide) def inspect = "" diff --git a/geom.rb b/geom.rb index 58253e2..e022c0f 100644 --- a/geom.rb +++ b/geom.rb @@ -8,16 +8,26 @@ def gap(geom,g) geom end -def split_geom(geom, dir, node, gap) +def split_geom(geom, dir, node, gap, ratio) geom = geom.dup case dir when :lr - geom.width = (geom.width-gap)/2 - geom.x += geom.width+gap if node == 1 + lw = (geom.width-gap)*ratio + if node == 0 + geom.width = lw + else + geom.width = (geom.width-gap)*(1.0-ratio) + geom.x += lw + gap + end return geom when :tb - geom.height = (geom.height-gap)/2 - geom.y += geom.height+gap if node == 1 + th = (geom.height-gap)*ratio + if node == 0 + geom.height = th + else + geom.height = (geom.height-gap)*(1.0-ratio) + geom.y += th + gap + end return geom else raise "Invalid direction" end diff --git a/node.rb b/node.rb index 4131dff..3798a72 100644 --- a/node.rb +++ b/node.rb @@ -1,14 +1,16 @@ class Node - attr_accessor :split, :nodes, :dir, :parent + attr_accessor :ratio, :nodes, :dir, :parent + attr_reader :geom - def inspect = "" + def inspect = "" def initialize(nodes=[], parent: nil, dir: nil) @nodes = Array(nodes.dup) @nodes.each{|n| n.parent = self } @parent = parent - @split = 0.5 + @ratio = 0.5 @dir = dir + @geom = nil # *current* geometry, subject to change at all time end # FIXME: Restate children, placements, find in terms of @@ -47,6 +49,7 @@ class Node @dir ||= dir dir = @dir nextdir = {lr: :tb, tb: :lr}[dir] + @geom = geom.dup case @nodes.length when 0 when 1 @@ -57,8 +60,8 @@ class Node end @nodes[0].layout(g, dir, level+1) when 2 - @nodes[0].layout(split_geom(geom, dir, 0,gap), gap, nextdir, level+1) - @nodes[1].layout(split_geom(geom, dir, 1,gap), gap, nextdir, level+1) + @nodes[0].layout(split_geom(geom, dir, 0,gap, @ratio), gap, nextdir, level+1) + @nodes[1].layout(split_geom(geom, dir, 1,gap, @ratio), gap, nextdir, level+1) else STDERR.puts "WARNING: Too many nodes" end diff --git a/rubywm.rb b/rubywm.rb index ec7645e..3ba81f6 100644 --- a/rubywm.rb +++ b/rubywm.rb @@ -18,16 +18,13 @@ Thread.abort_on_exception = true dpy = X11::Display.new # FIXME: This is a workaround for a deadlock -dpy.atom(:WM_CLASS) -dpy.atom(:STRING) +#dpy.atom(:WM_CLASS) +#dpy.atom(:STRING) -$wm = WindowManager.new(dpy, num_desktops: 10) +wm = WindowManager.new(dpy, num_desktops: 10) -start = nil -attr = nil - -d = TypeDispatcher.new($wm) +d = TypeDispatcher.new(wm) d.on(:client_message) do |ev| data = ev.data.unpack("V*") name = dpy.get_atom_name(ev.type) @@ -36,7 +33,6 @@ d.on(:client_message) do |ev| end loop do - # FIXME: Select properly ev = nil begin ev = dpy.next_packet @@ -47,58 +43,5 @@ loop do end p ev - - case ev - when X11::Form::ButtonPress - if ev.child # Whichever button, we want to know more about this window - w = $wm.window(ev.child) - attr = w.get_geometry rescue nil - if attr - $wm.set_focus(w.wid) - start = ev - end - end - when X11::Form::MotionNotify # if start.button == 1 we move; if 3 we resize, all with the same request: - # TODO: If floating, do this; if tiling, find neighours and resize them too. - - if ev.child != start.child - $wm.set_focus(ev.child) - end - - p [:MOTION, start, attr] - if start - xdiff = ev.root_x - start.root_x; - ydiff = ev.root_y - start.root_y; - - if start&.child && attr - w = $wm.window(start.child) - - # FIXME: Any other types we don't want to allow moving or resizing - begin - next if w.special? - rescue # FIXME - end - p w - if start.detail == 1 # Move - w.configure(x: attr.x + xdiff, y: attr.y + ydiff) - elsif start.detail == 3 # Resize - # If left/above the centre point, we grow/shrink the window to the left/top - # otherwise to the right/bottom. Doing it to the left/top requires - # moving it at the same time. - attr.x = attr.x + (ev.event_x-attr.x < attr.width / 2 ? xdiff : 0) - attr.y = attr.y + (ev.event_y-attr.y < attr.height/ 2 ? ydiff : 0) - attr.width = attr.width + (ev.event_x-attr.x < attr.width / 2 ? -xdiff : xdiff) - attr.height = attr.height+ (ev.event_y-attr.y < attr.height / 2 ? -ydiff : ydiff) - start.root_x = ev.root_x - start.root_y = ev.root_y - w.configure(x: attr.x, y: attr.y, width: attr.width, height: attr.height) - end - end - end - when X11::Form::ButtonRelease - # Make sure we don't accidentally operate on another window. - start.child = nil if start - else - d.(ev.class, ev) - end + d.(ev.class, ev) end diff --git a/wm.rb b/wm.rb index 21b3038..b03ea0f 100644 --- a/wm.rb +++ b/wm.rb @@ -23,6 +23,7 @@ class WindowManager # FIXME: Config # FIXME: Improved way of specifying pre-designed layouts. r = desktops[1].layout.root + r.ratio = 0.5 r.nodes[0] = Leaf.new(iclass: "todo-todo") r.nodes[1] = Node.new([ Leaf.new(iclass: "todo-done"), @@ -214,7 +215,7 @@ class WindowManager # FIXME: Switch focus (keep focus stack per desktop) old.hide change_property(:_NET_CURRENT_DESKTOP, :cardinal, d) - f = current_desktop&.children&.find {|w| !w.special?} + f = current_desktop&.mapped_regular_children&.first set_focus(f.wid) if f end @@ -238,7 +239,7 @@ class WindowManager # FIXME: This needs tweaks. Especially for floating windows, where # what we really want is to e.g. treat partially overlapping windows # so that the one closest to *overlapping* the correct border is picked - def find_closest(w, dir, from = windows.values) + def find_closest(w, dir, from) g = w.get_geometry case dir @@ -252,8 +253,6 @@ class WindowManager list = [] p [:here] from.each do |win| - next if win.special? - next if !win.mapped p [:checking, win, dir] g2 = win.get_geometry rescue nil next if g2.nil? @@ -312,6 +311,114 @@ class WindowManager p dpy.get_atom_name(ev.atom) rescue nil end + def on_button_press(ev) + return if !ev.child + w = window(ev.child) + @attr = w.get_geometry rescue nil + if @attr + set_focus(w.wid) + @start = ev + end + end + + # FIXME: This does not yet handle tiling + def on_motion_notify(ev) + # @start.button == 1 -> move + # @start.button == 3 -> resize + if ev.child != @start.child + set_focus(ev.child) rescue nil # FIXME + end + return if !@start&.child || !@attr + + xdiff = ev.root_x - @start.root_x; + ydiff = ev.root_y - @start.root_y; + + w = window(@start.child) + + # FIXME: Any other types we don't want to allow moving or resizing + begin + return if w.special? + rescue # FIXME: Why is this here? + end + + #p w + if @start.detail == 1 # Move + w.configure(x: attr.x + xdiff, y: attr.y + ydiff) + elsif @start.detail == 3 # Resize + lr = (ev.event_x-@attr.x < @attr.width / 2) + tb = (ev.event_y-@attr.y < @attr.height/ 2) + if w.floating? + # If left/above the centre point, we grow/shrink the window to the left/top + # otherwise to the right/bottom. Doing it to the left/top requires + # moving it at the same time. + @attr.x = @attr.x + (lr ? xdiff : 0) + @attr.y = @attr.y + (tb ? ydiff : 0) + @attr.width = @attr.width + (lr ? -xdiff : xdiff) + @attr.height = @attr.height+ (tb ? -ydiff : ydiff) + @start.root_x = ev.root_x + @start.root_y = ev.root_y + w.configure(x: @attr.x, y: @attr.y, width: @attr.width, height: @attr.height) + else + layout = current_desktop&.layout + if layout + prev = layout.find(w) + node = prev.parent + p :RESIZE + p [prev, node, layout.root] + while node #&& node != layout.root + p [lr, tb, node.dir, node] + if !lr.nil? && node.dir == :lr + dx = 0 + if node.geom + dx = (((node.geom.width * node.ratio) + xdiff)/node.geom.width) - node.ratio + else + dx = 0.00 + end + if node.nodes[0] == prev && !lr + # FIXME: This doesn't seem to matter + node.ratio += dx + @start.root_x = ev.root_x + lr = nil + elsif node.nodes[1] == prev + node.ratio += dx + @start.root_x = ev.root_x + lr = nil + end + end + if !tb.nil? && node.dir == :tb + dy = 0 + if node.geom + dy = (((node.geom.height * node.ratio) + ydiff)/node.geom.height) - node.ratio + else + dy = 0.00 + end + if node.nodes[0] == prev && !tb + # FIXME: This doesn't seem to matter + node.ratio += dy + @start.root_y = ev.root_y + node.ratio.clamp(0.1,0.9) + tb = nil + elsif node.nodes[1] == prev + node.ratio += dy + @start.root_y = ev.root_y + tb = nil + end + end + node.ratio = node.ratio.clamp(0.1,0.9) + prev = node + node = node.parent + end + p :RESIZE_DONE + layout.call + end + end + end + end + + def on_button_release(ev) + @start.child = nil if @start + end + def on_focus_in(ev) = (set_focus(ev.event) if !focus) def on_enter_notify(ev) = set_focus(ev.event) def on_unmap_notify(ev) = window(ev.window)&.desktop&.update_layout @@ -366,7 +473,7 @@ class WindowManager def on_rwm_focus(_, dir) dir = dpy.get_atom_name(dir).downcase.to_sym return if !@focus || @focus.special? - w = find_closest(@focus, dir, @focus.desktop.children) + w = find_closest(@focus, dir, @focus.desktop.mapped_regular_children) set_focus(w.wid) if w end @@ -418,7 +525,7 @@ class WindowManager return end - w = find_closest(@focus, dir, @focus.desktop.children.find_all{_1.mapped}) + w = find_closest(@focus, dir, @focus.desktop.mapped_regular_children) l1 = @focus.desktop&.layout&.find(@focus) l2 = w&.desktop&.layout&.find(w) @@ -438,5 +545,4 @@ class WindowManager set_focus(@focus.wid) end end - end