feat(dock): configurable position (bottom/left/right)
This commit is contained in:
5 files changed
+213
-36
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user