From 4580e57f3c10341be82196e01b0478b4f7d47014 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 13 Aug 2025 16:45:17 +0545 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(dock):=20implement=20hover=20e?= =?UTF-8?q?ffects=20and=20visual=20feedback=20for=20dock=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/dock.py | 221 +++++++++++++++++++++++++++++++++--------------- styles/dock.css | 99 +++++++++++++++++----- 2 files changed, 232 insertions(+), 88 deletions(-) diff --git a/modules/dock.py b/modules/dock.py index c3dbf8d..77914b1 100644 --- a/modules/dock.py +++ b/modules/dock.py @@ -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}") diff --git a/styles/dock.css b/styles/dock.css index 9286694..ac20672 100644 --- a/styles/dock.css +++ b/styles/dock.css @@ -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; }