✨ feat(dock): implement hover effects and visual feedback for dock items

This commit is contained in:
sankalp committed 2025-08-13 16:45:17 +05:45
1 parent acf651899e
commit 4580e57f3c
2 files changed
+232 -88

No files matched your search

+152 -69
View File
@@ -1,9 +1,9 @@
from gi.repository import GLib, Gtk
import json
import os
import re
import subprocess
from fabric.utils.helpers import get_desktop_applications, get_relative_path
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.eventbox import EventBox
@@ -11,16 +11,15 @@ from fabric.widgets.image import Image
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from fabric.widgets.revealer import Revealer
from fabric.widgets.separator import Separator
from widgets.wayland import WaylandWindow as Window
from gi.repository import GLib, Gtk
from loguru import logger
from utils.icon_resolver import IconResolver
from utils.occlusion import check_occlusion
from fabric.utils.helpers import get_relative_path, get_desktop_applications
import config.data as data
from services.modus import modus_service
from utils.functions import read_json_file, write_json_file
import config.data as data
from utils.icon_resolver import IconResolver
from utils.occlusion import check_occlusion
from widgets.wayland import WaylandWindow as Window
# Pinned apps file
PINNED_APPS_FILE = get_relative_path("../config/assets/dock.json")
@@ -30,11 +29,16 @@ class AppBar(Box):
def __init__(self, parent: Window):
self.client_buttons = {} # For running app instances
self.pinned_buttons = {} # For pinned apps
# Position tracking for hover effects
self.running_items_pos = []
self.pinned_items_pos = []
self._parent = parent
# Set orientation based on dock position
orientation = "vertical" if data.DOCK_POSITION in ["Left", "Right"] else "horizontal"
orientation = (
"vertical" if data.DOCK_POSITION in ["Left", "Right"] else "horizontal"
)
super().__init__(
spacing=10,
name="dock",
@@ -48,13 +52,15 @@ class AppBar(Box):
self.menu = Gtk.Menu()
self.pinned_apps = read_json_file(PINNED_APPS_FILE) or []
self.pinned_apps_container = Box(name="pinned-apps-container")
self.pinned_apps_container = Box()
self.add(self.pinned_apps_container)
self.separator = Separator(name="dock-separator")
self.separator = Box(
v_align="center", style_classes=["hidden", "dock_separator"]
)
self.add(self.separator)
self.running_apps_container = Box(name="running-apps-container")
self.running_apps_container = Box(name="dock_container")
self.add(self.running_apps_container)
self._populate_pinned_apps()
@@ -76,6 +82,7 @@ class AppBar(Box):
self.pinned_apps_container.remove(child)
self.pinned_buttons = {}
self.pinned_items_pos = []
try:
desktop_apps = get_desktop_applications(include_hidden=False)
@@ -109,28 +116,35 @@ class AppBar(Box):
display_name = app.display_name or app.name
icon_pixbuf = app.get_icon_pixbuf(data.DOCK_ICON_SIZE)
pinned_image = Image(pixbuf=icon_pixbuf)
pinned_image = Image(name="dock_item_icon")
pinned_image.set_from_pixbuf(icon_pixbuf)
icon_container = Box(
name="dock-icon",
orientation="vertical",
h_align="center",
main_container = Box(
name="dock_item_main_container",
orientation="v",
children=[pinned_image],
)
overlay_container = Overlay(name="dock-app-overlay", child=icon_container)
pinned_button = Button(
name="dock-app-button",
child=overlay_container,
name="dock_item",
child=main_container,
tooltip_text=display_name,
on_button_press_event=lambda _, event: self._handle_pinned_app_click(
event, app_data
),
on_enter_notify_event=lambda *_: self._handle_item_hovered(
pinned_button, True
),
on_leave_notify_event=lambda *_: self._handle_item_unhovered(
pinned_button, True
),
)
pinned_button.add_style_class("shown")
self.pinned_buttons[app_identifier] = pinned_button
self.pinned_apps_container.add(pinned_button)
self.pinned_items_pos.append(pinned_button)
def _find_desktop_app_from_data(self, app_data: dict, desktop_apps):
for app in desktop_apps:
@@ -229,6 +243,46 @@ class AppBar(Box):
self.show_menu(app_identifier)
self.menu.popup_at_pointer(event)
def _handle_item_hovered(self, item, pinned=False):
if pinned:
try:
index = self.pinned_items_pos.index(item)
if index > 0:
self.pinned_items_pos[index - 1].add_style_class("semi_hovered")
if index < len(self.pinned_items_pos) - 1:
self.pinned_items_pos[index + 1].add_style_class("semi_hovered")
except ValueError:
pass
else:
try:
index = self.running_items_pos.index(item)
if index > 0:
self.running_items_pos[index - 1].add_style_class("semi_hovered")
if index < len(self.running_items_pos) - 1:
self.running_items_pos[index + 1].add_style_class("semi_hovered")
except ValueError:
pass
def _handle_item_unhovered(self, item, pinned=False):
if pinned:
try:
index = self.pinned_items_pos.index(item)
if index > 0:
self.pinned_items_pos[index - 1].remove_style_class("semi_hovered")
if index < len(self.pinned_items_pos) - 1:
self.pinned_items_pos[index + 1].remove_style_class("semi_hovered")
except ValueError:
pass
else:
try:
index = self.running_items_pos.index(item)
if index > 0:
self.running_items_pos[index - 1].remove_style_class("semi_hovered")
if index < len(self.running_items_pos) - 1:
self.running_items_pos[index + 1].remove_style_class("semi_hovered")
except ValueError:
pass
def _get_app_identifier(self, app_data):
if isinstance(app_data, dict):
return app_data.get("name", "") or app_data.get("window_class", "")
@@ -298,7 +352,6 @@ class AppBar(Box):
"command_line": app.command_line,
}
else:
# Fallback: create minimal app data
app_data = {
"name": app_class,
"display_name": app_class,
@@ -395,9 +448,9 @@ class AppBar(Box):
button = self.client_buttons[instance_address]
if instance_address == focused_address:
button.add_style_class("active")
button.add_style_class("activated")
else:
button.remove_style_class("active")
button.remove_style_class("activated")
self._update_pinned_apps_state(clients)
self._update_separator_visibility()
@@ -428,10 +481,24 @@ class AppBar(Box):
for instance_id in buttons_to_remove:
button = self.client_buttons.pop(instance_id)
self.running_apps_container.remove(button)
try:
if button in self.running_items_pos:
self.running_items_pos.remove(button)
button.remove_style_class("shown")
except Exception:
pass
def remove_later():
try:
self.running_apps_container.remove(button)
except Exception:
pass
return False
GLib.timeout_add(250, remove_later)
def create_instance_button(self, instance_address, client, app_class):
client_image = Image()
client_image = Image(name="dock_item_icon")
try:
desktop_apps = get_desktop_applications(include_hidden=False)
@@ -440,7 +507,9 @@ class AppBar(Box):
if desktop_app:
pixbuf = desktop_app.get_icon_pixbuf(data.DOCK_ICON_SIZE)
else:
pixbuf = self.icon_resolver.get_icon_pixbuf(app_class, data.DOCK_ICON_SIZE)
pixbuf = self.icon_resolver.get_icon_pixbuf(
app_class, data.DOCK_ICON_SIZE
)
client_image.set_from_pixbuf(pixbuf)
except Exception as e:
@@ -460,35 +529,41 @@ class AppBar(Box):
if workspace_label:
image_overlay.add_overlay(workspace_label)
icon_container = Box(
name="dock-icon",
orientation="vertical",
h_align="center",
children=[image_overlay],
indicator = Box(name="dock_item_indicator", h_align="center")
main_container = Box(
name="dock_item_main_container",
orientation="v",
children=[image_overlay, indicator],
)
overlay_container = Overlay(name="dock-app-overlay", child=icon_container)
tooltip_text = client.get("title", app_class)
if tooltip_text != app_class:
tooltip_text = f"{app_class}: {tooltip_text}"
client_button = Button(
name="dock-app-button",
child=overlay_container,
name="dock_item",
child=main_container,
tooltip_text=tooltip_text,
on_button_press_event=lambda widget, event: self.handle_instance_click(
widget, event
),
on_enter_notify_event=lambda *_: self._handle_item_hovered(
client_button, False
),
on_leave_notify_event=lambda *_: self._handle_item_unhovered(
client_button, False
),
)
client_button.instance_address = instance_address
client_button.client_data = client
client_button.app_class = app_class
client_button.workspace_label = workspace_label
client_button.add_style_class("shown")
self.client_buttons[instance_address] = client_button
self.running_apps_container.add(client_button)
self.running_items_pos.append(client_button)
def _get_workspace_id(self, client):
workspace_data = client.get("workspace", {})
@@ -499,7 +574,6 @@ class AppBar(Box):
return None
def _is_special_workspace_id(self, ws_id):
"""Check if a workspace ID represents a special workspace"""
try:
# Convert to int if it's a string
workspace_id = int(ws_id)
@@ -512,14 +586,13 @@ class AppBar(Box):
return False
def _should_show_app_instance(self, client):
"""Determine if an app instance should be shown based on workspace and configuration"""
if not data.DOCK_HIDE_SPECIAL_WORKSPACE_APPS:
return True
workspace_id = self._get_workspace_id(client)
if workspace_id is None:
return True
return not self._is_special_workspace_id(workspace_id)
def update_instance_button(self, instance_address, client, app_class):
@@ -538,30 +611,28 @@ class AppBar(Box):
workspace_id = self._get_workspace_id(client)
existing_label = getattr(button, "workspace_label", None)
overlay_container = button.get_child()
if isinstance(overlay_container, Overlay):
icon_container = overlay_container.get_child()
if hasattr(icon_container, "get_children"):
children = icon_container.get_children()
if children:
image_overlay = children[0]
if isinstance(image_overlay, Overlay):
# Remove existing workspace label
if existing_label and existing_label.get_parent():
image_overlay.remove_overlay(existing_label)
container = button.get_child()
if hasattr(container, "get_children"):
children = container.get_children()
if children:
image_overlay = children[0]
if isinstance(image_overlay, Overlay):
# Remove existing workspace label
if existing_label and existing_label.get_parent():
image_overlay.remove_overlay(existing_label)
# Add new workspace label if needed
if workspace_id is not None:
new_label = Label(
label=str(workspace_id),
name="workspace-indicator",
h_align="end",
v_align="end",
)
image_overlay.add_overlay(new_label)
button.workspace_label = new_label
else:
button.workspace_label = None
# Add new workspace label if needed
if workspace_id is not None:
new_label = Label(
label=str(workspace_id),
name="workspace-indicator",
h_align="end",
v_align="end",
)
image_overlay.add_overlay(new_label)
button.workspace_label = new_label
else:
button.workspace_label = None
def handle_instance_click(self, button_widget, event):
instance_address = getattr(button_widget, "instance_address", None)
@@ -592,9 +663,12 @@ class AppBar(Box):
)
def _update_separator_visibility(self):
has_pinned_apps = len(self.pinned_buttons) > 0
has_running_apps = len(self.client_buttons) > 0
self.separator.set_visible(has_pinned_apps and has_running_apps)
has_pinned_apps = len(self.pinned_items_pos) > 0
has_running_apps = len(self.running_items_pos) > 0
if has_pinned_apps and has_running_apps:
self.separator.remove_style_class("hidden")
else:
self.separator.add_style_class("hidden")
class Dock(Window):
@@ -609,9 +683,9 @@ class Dock(Window):
super().__init__(layer="top", anchor=anchor)
self.app_bar = AppBar(self)
transition_type = self._get_transition_type()
self.revealer = Revealer(
child=Box(children=[self.app_bar], style="padding: 20px 50px 5px 50px;"),
transition_duration=500,
@@ -626,11 +700,12 @@ class Dock(Window):
)
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
self.dock_height = 100
self.is_hovered = False
self.hide_timeout_id = None
# Only setup occlusion monitoring if auto-hide is enabled
if data.DOCK_AUTO_HIDE:
self.setup_occlusion_monitoring()
@@ -641,9 +716,11 @@ class Dock(Window):
GLib.source_remove(self.hide_timeout_id)
self.hide_timeout_id = None
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
def on_hover_leave(self):
self.is_hovered = False
# Revealer/occlusion logic will handle CSS state
def _get_anchor_from_position(self):
if data.DOCK_POSITION == "Left":
@@ -684,8 +761,14 @@ class Dock(Window):
and self.revealer.get_reveal_child()
):
self.revealer.set_reveal_child(False)
self.app_bar.remove_style_class("shown")
elif not is_occluded and not self.revealer.get_reveal_child():
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
elif is_occluded and self.is_hovered:
if not self.revealer.get_reveal_child():
self.revealer.set_reveal_child(True)
self.app_bar.add_style_class("shown")
except Exception as e:
logger.error(f"[Dock] Occlusion check error: {e}")
+80 -19
View File
@@ -7,31 +7,92 @@
transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
#dock-app-button {
border-radius: 16px;
#dock.shown {
transition: margin 0.4s cubic-bezier(0.2, 1.8, 0.5, 0.8);
}
#dock-separator {
min-width: 3px;
margin: 2px 0;
background-color: var(--shadow);
border-radius: 8px;
#dock_item_main_container {
transition: all 0.15s ease-out;
}
#dock-app-overlay label {
background-color: alpha(#000, 0.5);
font-size: 12px;
margin-top: 20px;
min-width: 15px;
border-radius: 8px;
#dock_item.shown:hover #dock_item_main_container {
margin-top: -12px;
}
#dock-app-button:hover,
#dock-app-button:hover.instance {
background-color: var(--surface-bright);
#dock_item.shown.semi_hovered #dock_item_main_container {
margin-top: -7px;
}
#dock-app-button:active,
#dock-app-button:active.instance {
background-color: #89b4fa;
#dock_item.shown.semi_hovered #dock_item_indicator {
margin-top: 11px;
}
#dock_item.shown.activated:hover #dock_item_main_container {
margin-top: -12px;
}
#dock_item.shown.activated:hover #dock_item_indicator {
margin-top: 16px;
}
#dock_item.shown.activated #dock_item_main_container {
margin-top: -4px;
}
#dock_item.shown.activated #dock_item_icon {
opacity: 1;
/* background-color: #01458e; */
}
#dock_item.shown.activated #dock_item_indicator {
min-width: 4px;
margin-top: 8px;
background-color: #aac7ff;
}
#dock_item_icon {
border-radius: 13px;
opacity: 0.7;
}
#dock_item_indicator {
transition: all 0.15s ease-out;
min-height: 4px;
border-radius: 4px;
}
#dock_item {
transition: margin 0.2s ease-out;
}
#dock_item.shown {
transition: margin 0.2s ease-out;
padding-left: 5px;
opacity: 1;
}
#dock_item.shown:first-child {
padding-left: 0px;
}
.dock_separator {
transition: all 0.1s ease-out;
min-width: 1.5px;
min-height: 50px;
border-radius: 2px;
background-color: #2b5ea7;
margin: 0px 2px;
}
.dock_separator.hidden {
min-width: 0px;
background-color: transparent;
margin: 0px;
}
#dock_item.shown:not(.activated) {
min-width: 0px;
min-height: 0px;
margin-top: 0px;
background-color: transparent;
}