feat(dock): configurable position (bottom/left/right)

This commit is contained in:
sankalp committed 2026-08-09 13:23:58 +05:45
1 parent 090044c35d
commit 79a7e58198
5 files changed
+213 -36

No files matched your search

+3
View File
@@ -11,6 +11,7 @@ DEFAULT = {
},
"dock": {
"enabled": True,
"position": "bottom",
"auto_hide": True,
"always_occluded": False,
"icon_size": 52,
@@ -62,6 +63,8 @@ def generate_default_toml() -> str:
dock = _table()
dock.add("enabled", True)
dock.add("position", "bottom")
dock["position"].comment("'bottom' | 'left' | 'right'")
dock.add("auto_hide", True)
dock.add("always_occluded", False)
dock.add("icon_size", 52)
+70 -24
View File
@@ -45,6 +45,8 @@ from .constants import (
PINNED_APPS_FILE,
SEPARATOR_COLOR,
SEPARATOR_WIDTH,
dock_position,
is_vertical,
max_scale,
)
from .items import DockHitTest, DockItem, DockModel
@@ -140,13 +142,13 @@ class DockCanvas(Gtk.DrawingArea):
self._desktop_apps = []
return self._desktop_apps
def _canvas_height(self) -> int:
def _canvas_edge_thickness(self) -> int:
size = self._base_icon_size()
return int(
size * max_scale() + CANVAS_TOP_PAD + 2 * BG_PADDING_V + INDICATOR_H + 4
)
def _canvas_max_width(self) -> int:
def _canvas_edge_length(self) -> int:
n = len(self.model.items)
if n == 0:
return 200
@@ -155,6 +157,16 @@ class DockCanvas(Gtk.DrawingArea):
n * size * max_scale() + max(n - 1, 0) * ICON_GAP + 2 * BG_PADDING_H + 40
)
def _canvas_height(self) -> int:
if is_vertical():
return self._canvas_edge_length()
return self._canvas_edge_thickness()
def _canvas_max_width(self) -> int:
if is_vertical():
return self._canvas_edge_thickness()
return self._canvas_edge_length()
def _canvas_min_width(self) -> int:
return self._canvas_max_width()
@@ -545,7 +557,7 @@ class DockCanvas(Gtk.DrawingArea):
self._draw_background(cr, bg_x, bg_y, bg_w, bg_h)
if self.model.has_pinned() and self.model.has_running_only():
self._draw_separator(cr, items, bg_y, bg_h, base_size)
self._draw_separator(cr, items, bg_x, bg_y, bg_w, bg_h)
for item in items:
self._draw_item(cr, item, base_size, h)
@@ -592,29 +604,39 @@ class DockCanvas(Gtk.DrawingArea):
self,
cr: cairo.Context,
items: List[DockItem],
bg_x: float,
bg_y: float,
bg_w: float,
bg_h: float,
base_size: int,
) -> None:
last_pinned_x: Optional[float] = None
first_running_x: Optional[float] = None
vertical = is_vertical()
last_pinned_c: Optional[float] = None
first_running_c: Optional[float] = None
for item in items:
if item.is_pinned:
last_pinned_x = item.render_x + item.render_w
elif first_running_x is None:
first_running_x = item.render_x
last_pinned_c = (
item.render_y + item.render_h
if vertical
else item.render_x + item.render_w
)
elif first_running_c is None:
first_running_c = item.render_y if vertical else item.render_x
if last_pinned_x is None or first_running_x is None:
if last_pinned_c is None or first_running_c is None:
return
sep_x = (last_pinned_x + first_running_x) / 2.0
sep_c = (last_pinned_c + first_running_c) / 2.0
sep_margin = BG_PADDING_V + 4
cr.save()
cr.set_source_rgba(*SEPARATOR_COLOR)
cr.set_line_width(SEPARATOR_WIDTH)
cr.move_to(sep_x, bg_y + sep_margin)
cr.line_to(sep_x, bg_y + bg_h - sep_margin)
if vertical:
cr.move_to(bg_x + sep_margin, sep_c)
cr.line_to(bg_x + bg_w - sep_margin, sep_c)
else:
cr.move_to(sep_c, bg_y + sep_margin)
cr.line_to(sep_c, bg_y + bg_h - sep_margin)
cr.stroke()
cr.restore()
@@ -664,10 +686,14 @@ class DockCanvas(Gtk.DrawingArea):
cr.restore()
if item.instance_address and item.instance_address == self._focused_address:
dot_cx = ix + iw / 2.0
dot_cy = iy + ih + 4
cr.save()
cr.set_source_rgba(*INDICATOR_COLOR)
if is_vertical():
dot_cx = ix + iw + 4 if dock_position() == "right" else ix - 4
dot_cy = iy + ih / 2.0
else:
dot_cx = ix + iw / 2.0
dot_cy = iy + ih + 4
cr.arc(dot_cx, dot_cy, INDICATOR_RADIUS, 0, 2 * math.pi)
cr.fill()
cr.restore()
@@ -715,16 +741,36 @@ class DockCanvas(Gtk.DrawingArea):
n = len(items)
total_base = n * size + max(n - 1, 0) * ICON_GAP
w = self.get_allocated_width()
base_start_x = (w - total_base) / 2.0
h = self.get_allocated_height()
bg_h = size + 2 * BG_PADDING_V
bg_y = h - INDICATOR_H - bg_h
baseline_y = bg_y + BG_PADDING_V + size
icon_top_y = baseline_y - size * max_scale()
self._mouse_inside = (
base_start_x <= event.x <= base_start_x + total_base
and icon_top_y <= event.y <= baseline_y
)
if is_vertical():
base_start_y = (h - total_base) / 2.0
bg_w = size + 2 * BG_PADDING_V
if dock_position() == "right":
bg_x = w - INDICATOR_H - bg_w
baseline_x = bg_x + BG_PADDING_V + size
icon_left_x = baseline_x - size * max_scale()
self._mouse_inside = (
icon_left_x <= event.x <= baseline_x
and base_start_y <= event.y <= base_start_y + total_base
)
else:
bg_x = INDICATOR_H
baseline_x = bg_x + BG_PADDING_V + size
self._mouse_inside = (
bg_x <= event.x <= baseline_x + size * max_scale()
and base_start_y <= event.y <= base_start_y + total_base
)
else:
base_start_x = (w - total_base) / 2.0
bg_h = size + 2 * BG_PADDING_V
bg_y = h - INDICATOR_H - bg_h
baseline_y = bg_y + BG_PADDING_V + size
icon_top_y = baseline_y - size * max_scale()
self._mouse_inside = (
base_start_x <= event.x <= base_start_x + total_base
and icon_top_y <= event.y <= baseline_y
)
else:
self._mouse_inside = False
+12
View File
@@ -3,6 +3,18 @@ from utils.gtk_utils import toml_file
PINNED_APPS_FILE = toml_file("dock.toml")
_POSITIONS = ("bottom", "left", "right")
def dock_position() -> str:
position = str(config().get("dock.position", "bottom")).lower()
return position if position in _POSITIONS else "bottom"
def is_vertical() -> bool:
return dock_position() in ("left", "right")
LERP_FACTOR = 0.20
IDLE_THRESHOLD = 0.0005
ANIM_FPS = 60
+72 -5
View File
@@ -7,6 +7,8 @@ from .constants import (
INDICATOR_H,
MIN_SCALE,
SIGMA_FACTOR,
dock_position,
is_vertical,
max_scale,
)
from .items import DockItem
@@ -17,7 +19,7 @@ class DockLayout:
def compute(
items: list[DockItem],
mouse_x: float,
_mouse_y: float,
mouse_y: float,
base_icon_size: int,
canvas_w: int,
canvas_h: int,
@@ -26,19 +28,24 @@ class DockLayout:
if not items:
return
vertical = is_vertical()
sigma = base_icon_size * SIGMA_FACTOR
amplitude = max_scale() - MIN_SCALE
n = len(items)
base_w = base_icon_size + ICON_GAP
base_unit = base_icon_size + ICON_GAP
total_base = n * base_icon_size + max(n - 1, 0) * ICON_GAP
start_x = (canvas_w - total_base) / 2.0
start = (
((canvas_h - total_base) / 2.0)
if vertical
else ((canvas_w - total_base) / 2.0)
)
for i, item in enumerate(items):
est_cx = start_x + i * base_w + base_icon_size / 2.0
est_c = start + i * base_unit + base_icon_size / 2.0
if mouse_inside:
dist = abs(est_cx - mouse_x)
dist = abs(est_c - (mouse_y if vertical else mouse_x))
target = MIN_SCALE + amplitude * math.exp(
-(dist * dist) / (2.0 * sigma * sigma)
)
@@ -47,6 +54,16 @@ class DockLayout:
item.target_scale = target
if vertical:
DockLayout._layout_vertical(items, base_icon_size, canvas_w, canvas_h)
else:
DockLayout._layout_horizontal(items, base_icon_size, canvas_w, canvas_h)
@staticmethod
def _layout_horizontal(
items: list[DockItem], base_icon_size: int, canvas_w: int, canvas_h: int
) -> None:
n = len(items)
bg_h = base_icon_size + 2 * BG_PADDING_V
bg_y = canvas_h - INDICATOR_H - bg_h
baseline_y = bg_y + BG_PADDING_V + base_icon_size
@@ -67,6 +84,37 @@ class DockLayout:
item.drag_index = i
cursor_x += iw + ICON_GAP
@staticmethod
def _layout_vertical(
items: list[DockItem], base_icon_size: int, canvas_w: int, canvas_h: int
) -> None:
n = len(items)
bg_w = base_icon_size + 2 * BG_PADDING_V
position = dock_position()
rendered_heights = [base_icon_size * item.current_scale for item in items]
total_h = sum(rendered_heights) + max(n - 1, 0) * ICON_GAP
cursor_y = (canvas_h - total_h) / 2.0
if position == "right":
bg_x = canvas_w - INDICATOR_H - bg_w
anchor_x = bg_x + BG_PADDING_V + base_icon_size
else:
bg_x = INDICATOR_H
anchor_x = bg_x + BG_PADDING_V
for i, item in enumerate(items):
ih = rendered_heights[i]
iw = base_icon_size * item.current_scale
iy = cursor_y
ix = anchor_x - iw if position == "right" else anchor_x
item.render_x = ix
item.render_y = iy
item.render_w = iw
item.render_h = ih
item.drag_index = i
cursor_y += ih + ICON_GAP
@staticmethod
def background_rect(
items: list[DockItem],
@@ -74,6 +122,25 @@ class DockLayout:
canvas_w: int,
canvas_h: int,
) -> tuple[float, float, float, float]:
if is_vertical():
bg_w = base_icon_size + 2 * BG_PADDING_V
bg_x = (
canvas_w - INDICATOR_H - bg_w
if dock_position() == "right"
else INDICATOR_H
)
if items:
min_y = min(item.render_y for item in items)
max_y = max(item.render_y + item.render_h for item in items)
bg_y = min_y - BG_PADDING_H
bg_h = (max_y - min_y) + 2 * BG_PADDING_H
else:
bg_y = BG_PADDING_H
bg_h = canvas_h - 2 * BG_PADDING_H
return bg_x, bg_y, bg_w, bg_h
bg_h = base_icon_size + 2 * BG_PADDING_V
bg_y = canvas_h - INDICATOR_H - bg_h
+56 -7
View File
@@ -3,18 +3,20 @@ from fabric.widgets.box import Box
from fabric.widgets.eventbox import EventBox
from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
from gi.repository import Gtk
from services.config import config, on_config_change
from services.modus import check_occlusion, get_modus_service
from .canvas import DockCanvas
from .constants import dock_position, is_vertical
class Dock(Window):
def __init__(self):
super().__init__(
layer="top",
anchor="bottom center",
anchor=self._anchor_for_position(),
exclusivity="none",
title="modus-dock",
name="dock",
@@ -33,14 +35,15 @@ class Dock(Window):
self.revealer = Revealer(
child=self.canvas,
transition_duration=200,
transition_type="slide-up",
transition_type=self._transition_for_position(),
)
hover_strip = Box(
name="dock-hover-strip",
)
outer_box = Box(
self.hover_strip = hover_strip
self.outer_box = Box(
name="dock-outer-box",
orientation="v",
children=[self.revealer, hover_strip],
@@ -48,12 +51,13 @@ class Dock(Window):
self.children = EventBox(
events=["enter-notify", "leave-notify"],
child=outer_box,
child=self.outer_box,
on_enter_notify_event=lambda *_: self.on_hover_enter(),
on_leave_notify_event=lambda *_: self.on_hover_leave(),
)
self.dock_height = 100
self.dock_thickness = self.canvas._canvas_edge_thickness()
self._apply_position()
self.is_hovered = False
self.hide_ticket = 0
self._occlusion_timer_id = None
@@ -70,6 +74,47 @@ class Dock(Window):
self._setup_occlusion_signals()
self._check_occlusion_deferred()
@staticmethod
def _anchor_for_position() -> str:
return {
"left": "center left",
"right": "center right",
}.get(dock_position(), "bottom center")
@staticmethod
def _transition_for_position() -> str:
return {
"left": "slide-right",
"right": "slide-left",
}.get(dock_position(), "slide-up")
def _occlusion_region(self) -> tuple[str, int]:
position = dock_position()
if position in ("left", "right"):
return position, self.dock_thickness
return "bottom", self.dock_thickness
def _apply_position(self) -> None:
self.anchor = self._anchor_for_position()
self.revealer.transition_type = self._transition_for_position()
vertical = is_vertical()
self.outer_box.set_orientation(
Gtk.Orientation.HORIZONTAL if vertical else Gtk.Orientation.VERTICAL
)
if vertical:
children = (
[self.revealer, self.hover_strip]
if dock_position() == "right"
else [self.hover_strip, self.revealer]
)
else:
children = [self.revealer, self.hover_strip]
self.outer_box.children = children
self.dock_thickness = self.canvas._canvas_edge_thickness()
self.canvas._update_size_request()
def _setup_occlusion_signals(self) -> None:
"""Subscribe to Hyprland events that affect occlusion."""
hyprland = get_modus_service()._hyprland_connection
@@ -103,7 +148,7 @@ class Dock(Window):
if not config().get("dock_auto_hide", True):
return False
is_occ = config().get("dock.always_occluded", False) or check_occlusion(
("bottom", self.dock_height)
self._occlusion_region()
)
if is_occ and not self.is_hovered and self.revealer.get_reveal_child():
self.revealer.set_reveal_child(False)
@@ -120,6 +165,10 @@ class Dock(Window):
if config().has_changed("dock.enabled", old_config):
self._update_visibility()
if config().has_changed("dock.position", old_config):
self._apply_position()
self.canvas.update_icon_size()
if config().has_changed("dock.icon_size", old_config):
self.canvas.update_icon_size()
@@ -160,7 +209,7 @@ class Dock(Window):
if config().get("dock.auto_hide", True):
is_occ = config().get(
"dock.always_occluded", False
) or check_occlusion(("bottom", self.dock_height))
) or check_occlusion(self._occlusion_region())
if is_occ:
self.revealer.set_reveal_child(False)
return False