diff --git a/desktopenvs/hyprlua/astal-menu/ui/taskbar.py b/desktopenvs/hyprlua/astal-menu/ui/taskbar.py index 4dbe419..fc4efd9 100644 --- a/desktopenvs/hyprlua/astal-menu/ui/taskbar.py +++ b/desktopenvs/hyprlua/astal-menu/ui/taskbar.py @@ -279,9 +279,13 @@ class Taskbar(Gtk.Box): row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) row.append(Gtk.Label(label="center-focused", xalign=0.0, hexpand=True)) sw = Gtk.Switch(valign=Gtk.Align.CENTER) - sw.set_tooltip_text("Keep the focused column centred so the prev/next " - "columns stay on-screen and tappable") - sw.connect("state-set", self._on_fit_toggle) + if ly["name"] == "columns": + sw.set_tooltip_text("Keep the focused window centred in its column as " + "it scrolls, instead of the minimal-movement default") + else: + sw.set_tooltip_text("Keep the focused column centred so the prev/next " + "columns stay on-screen and tappable") + sw.connect("state-set", lambda s, state, name=ly["name"]: self._on_fit_toggle(s, state, name)) row.append(sw) page.append(row) self._fit_sws[ly["name"]] = sw @@ -325,6 +329,15 @@ class Taskbar(Gtk.Box): self._cols_lbl.set_label(str(new)) _eval(f'hl.dispatch(hl.dsp.layout("cols {"+1" if delta > 0 else "-1"}"))') + def _read_center(self) -> bool: + # columns.lua's own "center-focused" switch — global, published in the same + # cache file as the columns stepper's count (see columns.lua's publish()). + try: + data = json.loads(_COLUMNS_STATE.read_text()) + return bool(data.get("_center", False)) + except (FileNotFoundError, json.JSONDecodeError, ValueError, TypeError): + return False + # -- sync + handlers --------------------------------------------------- def _sync_layout_controls(self) -> None: # reflect the current per-ws layout / direction / focus_fit in the tabs+options @@ -332,6 +345,11 @@ class Taskbar(Gtk.Box): run_json(["hyprctl", "getoption", "general:layout", "-j"], self._apply_tab_sel) run_json(["hyprctl", "getoption", "scrolling:direction", "-j"], self._apply_dir_sel) run_json(["hyprctl", "getoption", "scrolling:focus_fit_method", "-j"], self._apply_fit_sel) + # columns' own center-focused state lives in columns-state.json, not a hyprctl + # option — read it synchronously rather than round-tripping a subprocess. + columns_sw = self._fit_sws.get("columns") + if columns_sw is not None: + columns_sw.set_active(self._read_center()) def _apply_tab_sel(self, ok, data) -> None: cur = data.get("str") if ok and isinstance(data, dict) else None @@ -356,11 +374,15 @@ class Taskbar(Gtk.Box): self._syncing = False def _apply_fit_sel(self, ok, data) -> None: + # scrolling:focus_fit_method only describes the scrolling layout's own switch; + # columns' switch is synced separately from columns-state.json (see + # _sync_layout_controls) since it isn't backed by a hyprctl option at all. val = data.get("int") if ok and isinstance(data, dict) else 0 - self._syncing = True - for sw in self._fit_sws.values(): + sw = self._fit_sws.get("scrolling") + if sw is not None: + self._syncing = True sw.set_active(val == 1) - self._syncing = False + self._syncing = False def _cur_dir(self) -> str: entry = self._dir_dds.get(self._layout_stack.get_visible_child_name()) @@ -384,9 +406,12 @@ class Taskbar(Gtk.Box): if name: _eval(f'layouts.set("{self._active_ws}", "{name}", "{self._cur_dir()}")') - def _on_fit_toggle(self, _sw, state) -> bool: + def _on_fit_toggle(self, _sw, state, name) -> bool: if not getattr(self, "_syncing", False): - _eval(f"layouts.set_fit({1 if state else 0})") + if name == "columns": + _eval(f'hl.dispatch(hl.dsp.layout("center {"on" if state else "off"}"))') + else: + _eval(f"layouts.set_fit({1 if state else 0})") return False # -- window actions ---------------------------------------------------- diff --git a/desktopenvs/hyprlua/hypr/layouts/columns.lua b/desktopenvs/hyprlua/hypr/layouts/columns.lua index cbb6d82..0f10855 100644 --- a/desktopenvs/hyprlua/hypr/layouts/columns.lua +++ b/desktopenvs/hyprlua/hypr/layouts/columns.lua @@ -35,6 +35,15 @@ -- vertical ws: k/j = −/+ columns, h/l = −/+ per-view. -- cols +/-/N — direct column-count set (the menu's stepper). -- orient h/v — orientation (set from the menu dropdown). +-- center on/off/toggle — "center-focused" switch (menu): when a column is overfull +-- and scrolls, keep the focused window centred in the column +-- instead of the minimal-movement default (whichever edge of the +-- focused window would otherwise go off-screen just barely comes +-- back into view). Global (mirrors the scrolling layout's +-- "Centered follow" toggle), persisted across reloads, OFF by +-- default. Only ever repositions the column holding the focused +-- window — every other column's pan is untouched, since there is +-- only one focused window at a time. -- -- State is per workspace; a window's column assignment, order, both weight tables and -- each column's pan offset persist across relayouts. @@ -48,8 +57,22 @@ local function clampw(v) return math.max(RMIN, math.min(RMAX, v)) end -- Publish per-workspace column count so the astal-menu can show a live stepper. local STATE_FILE = (os.getenv("HOME") or "") .. "/.cache/astal-menu/columns-state.json" + +-- "center-focused" switch — global (like the scrolling layout's own fit-method +-- toggle), so a single state, not per-workspace. Seeded once at module load from +-- the last-published state, since a reload wipes this module's Lua state (same +-- reason ncols/rows/orient are seeded — see seed() below). +local center_focus = false +do + local f = io.open(STATE_FILE, "r") + if f then + local data = f:read("*a") or ""; f:close() + if data:match('"_center"%s*:%s*true') then center_focus = true end + end +end + local function publish() - local parts = {} + local parts = { string.format('"_center":%s', tostring(center_focus)) } for wsid, s in pairs(state) do parts[#parts + 1] = string.format('"%s":{"cols":%d,"rows":%d,"orient":"%s"}', tostring(wsid), s.ncols, s.rows, s.orient) @@ -258,15 +281,24 @@ local function recalculate(ctx) for r = 1, #rowlist do rh[r] = rh[r] * scale end pan = 0 else - -- overfull: keep natural heights and PAN so the focused row stays in view. + -- overfull: keep natural heights and PAN so the focused row stays in view + -- (center_focus: centred in the viewport instead of minimal movement). + -- Only the column holding the focused window (frow ~= nil) is touched here; + -- every other column keeps whatever pan it already had. pan = s.pan[c] or 0 if frow then local top = 0 for r = 1, frow - 1 do top = top + rh[r] end local bottom = top + rh[frow] - if top - pan < 0 then pan = top end - if bottom - pan > secLen then pan = bottom - secLen end + if center_focus then + pan = top + (rh[frow] - secLen) / 2 + else + if top - pan < 0 then pan = top end + if bottom - pan > secLen then pan = bottom - secLen end + end end + -- clamp to the column's actual scroll range either way — this is also what + -- keeps "centred" from showing blank space past the first/last row. pan = math.max(0, math.min(pan, total - secLen)) end s.pan[c] = pan @@ -300,11 +332,23 @@ local function recalculate(ctx) end local function layout_msg(ctx, cmd) + local verb, arg = tostring(cmd or ""):match("^(%S+)%s*(%S*)$") + if not verb then return end + + -- "center-focused" switch: global, doesn't touch any window, so handled before the + -- empty-workspace bail-out below (toggling it from the menu shouldn't require a + -- window to be focused/present). + if verb == "center" then + if arg == "on" then center_focus = true + elseif arg == "off" then center_focus = false + else center_focus = not center_focus end + publish() + return + end + local t = ctx.targets if type(t) ~= "table" or not t[1] then return end local s = ws_state(win_ws_id(t[1].window)) - local verb, arg = tostring(cmd or ""):match("^(%S+)%s*(%S*)$") - if not verb then return end local cols, _, foc = columnize(s, t) local horizontal = (s.orient ~= "v") @@ -426,6 +470,6 @@ return { dirs = { "h", "v" }, dir_labels = { "Horizontal", "Vertical" }, default_dir = "h", - fit_method = false, + fit_method = true, -- menu shows the "center-focused" switch (routed to "center on/off") stepper = true, -- menu shows a "Columns: (-)[N](+)" count stepper }