Scroll Jwno, Scroll!


Preface

I’ve been lurking around GlazeWM and Komorebi’s Discord servers for some time, mostly for seeing how others make use of a tiling window manager. In my personal experience, these are really welcoming communities, and I think Komorebi’s author himself is especially cool! If Jwno is too messy for you, please do consider these established solutions!

I mention this because someone from these communities (Hi @Gonkleman!) asked about turning Jwno into a “scrolling” WM like PaperWM or NiriWM. My first thought was, nope, this is not what Jwno is built for. But the idea is so intriguing I couldn’t stop thinking about it. Finally I decided I should try to do a PoC using Jwno’s existing API, and find out how well it could work.

The Theory

As I described in a previous post, Jwno’s top-level frames (which represent physical monitors) can be resized and moved around freely. They just have the same geometries as the monitors by default.

What if we expand the top frames to the left and right sides, instead of confining them to the screen’s work area? Can we “scroll” them around? The answers seem to be “yes”. (There won’t be any soothing animation though 😅)

Scrolling Horizontally

The first thing I tried is moving the top-level frame around, interactively. If you have Jwno running, you can try this out in the REPL too:

(defn cmd-move-top-frame [offset-x offset-y]
  (def top-frame
    (:get-current-top-frame (get-in jwno/context [:window-manager :root])))
  (def top-rect (in top-frame :rect))
  (def new-rect
    {:left (+ offset-x (in top-rect :left))
     :top (+ offset-y (in top-rect :top))
     :right (+ offset-x (in top-rect :right))
     :bottom (+ offset-y (in top-rect :bottom))})
  (:transform top-frame new-rect)
  (:retile (in jwno/context :window-manager) top-frame))

(:add-command (in jwno/context :command-manager) :move-top-frame cmd-move-top-frame)

(:define-key root-keymap "Win + Left" [:move-top-frame -100 0])
(:define-key root-keymap "Win + Up" [:move-top-frame 0 -100])
(:define-key root-keymap "Win + Right" [:move-top-frame 100 0])
(:define-key root-keymap "Win + Down" [:move-top-frame 0 100])

(:set-keymap (in jwno/context :key-manager) root-keymap)

And it works like this:

Hmm, not bad! I was worried that the windows moved off-screen would have strange behavior, but it seems Windows can handle them well enough, with a single monitor at least. Then I wrote a simple function to scroll a frame into view, and connected it to the :frame-activated hook:

(defn rect-center-x [rect]
  (math/floor (/ (+ (in rect :left)
                    (in rect :right))
                 2)))

(defn scroll-to [cur-frame]
  (def top-frame (:get-top-frame cur-frame))
  (def cur-rect (in cur-frame :rect))
  (def center-x (rect-center-x cur-rect))
  (def mon-rect (get-in top-frame [:monitor :work-area]))
  (def mon-center-x (rect-center-x mon-rect))
  (def offset-x (- mon-center-x center-x))
  (def top-rect (in top-frame :rect))
  (def new-rect {:left (+ offset-x (in top-rect :left))
                 :top (in top-rect :top)
                 :right (+ offset-x (in top-rect :right))
                 :bottom (in top-rect :bottom)})
  (:transform top-frame new-rect)
  top-frame)

(def scroll-to-hook
  (:add-hook (in jwno/context :hook-manager) :frame-activated
     (fn [frame]
       (:retile (in jwno/context :window-manager) (scroll-to frame)))))

Now the active frame will roll to the center of the screen automatically:

Not bad at all!

Expanding The Top-Level Frame

Currently, Jwno does most of the geometry calculations based on the assumption that a top-level frame would have a fixed size. Now that we want to have an infinite number of windows lined up off-screen, we need to throw this assumption out of the window.

To put us in context, here’s how Jwno inserts a frame:

  1. Insert a zero-width (or height) frame in the desired direction;

  2. Expand the newly inserted frame to the desired size, while shrinking its siblings in proportion, so that their parent frame retains the same size.

Now we should change step 2, to grow the parent frame, instead of shrinking the siblings. This is when I discovered one of the limitations of Jwno’s current API: The geometry calculation code is highly coupled with the frame objects, we can’t just replace it without writing some repetitive code.

Luckily, currently in Jwno, when a parent frame is resized, we can choose to also have the child frames resized in proportion.

So I took a shortcut instead, and added this extra step:

  1. Expand the parent frame again, so that the shrunken sibling frames are restored to their original sizes.

But we’ll need to calculate another intermediate size for the newly inserted frame in step 2, or its final size would be wrong after step 3.

OK that’s confusing enough, let me just show you the code:

(import jwno/util)

(defn insert-win-left [top-frame win new-width]
  (def top-rect (in top-frame :rect))
  (def [top-width top-height] (util/rect-size top-rect))
  (def ratio (/ new-width (+ new-width top-width)))
  #
  # This is step 1 and 2 combined.
  #
  (:insert-sub-frame top-frame 0 ratio)
  #
  # And this is step 3, we expand the parent frame by moving
  # its left border.
  #
  (def new-top-rect {:left (- (in top-rect :left) new-width)
                     :top (in top-rect :top)
                     :right (in top-rect :right)
                     :bottom (in top-rect :bottom)})
  (:transform top-frame new-top-rect)
  (put (in win :tags) :frame (first (in top-frame :children))))

Then we can simply add it to the :window-created hook:

(defn on-window-created [win]
  (def top-frame (:get-current-top-frame (get-in jwno/context [:window-manager :root])))
  #
  # ... handle some edge cases ...
  #
  (insert-win-left top-frame win 960))

(def insert-win-hook
  (:add-hook (in jwno/context :hook-manager) :window-created
     (fn [win & _]
       (on-window-created win))))

To simplify things a bit, I chose to always insert the new windows to the left-most or right-most sides. It works like this (note how the blank Notepad window is placed):

Now we have a working (well, kind of) scrolling WM! There are in fact more edge cases we need to handle, but those are not the fun parts, so I’ll pretend they don’t exist. I posted the full code, with some instructions on how to test it, as a gist.

Conclusion

When developing new features for Jwno, I often try to pretend that I only have access to Jwno’s public API, and see if a new feature can be built on top of that. This process is always fun, just like what we have done here. I can see a few problems by doing this PoC, but they’re quite gory so I won’t bother you with them here.

That said, I know Jwno inside out, so I’m probably the worst API tester available. If you’ve done something interesting with Jwno, or simply have ideas to share, please get in touch by leaving a comment, or by posting a Github Discussion.

Cheers! 🤘

Get Jwno

Buy Now$10.00 USD or more

Leave a comment

Log in with itch.io to leave a comment.