feat: smart PWA/Flutter app icon matching, magnification, launch bounce & theme radius - #2
Open
NothingManTR wants to merge 20 commits into
Open
Conversation
Clicking a dock icon went through the Wayland activate request, which only hands over keyboard focus. On Hyprland's scrolling layout that switches to the window's workspace and leaves the window itself parked off-screen, so the click reads as a no-op: with three columns on one workspace, the target sat at x=-1459 on a 1536px viewport and stayed there. Focus now goes through the compositor's own dispatcher, which moves the viewport with it (x=-1459 -> 12). The same path fixes wheel cycling, the right-click window list, and windows on special workspaces, and it degrades to the Wayland request when no Hyprland address is available. Around that: - Workspace hints. A tooltip names the workspace when the app's windows are all elsewhere (Slack [3]), and window rows in the context menu carry the same label ([3] Slack - DM). Read live off the Hyprland handles, so a window moving between workspaces needs no model rebuild. - Urgent highlights. A window asking for attention pulses its indicator and icon ring in the theme's urgent colour until it is focused. Toggle: showUrgentHint. - Launch feedback. A cold app pulses its icon until its window shows up, so one click does not become three. - Click active to minimize (opt-in, clickToMinimize). Clicking the app you are already in parks its single window on a hidden special:minimized workspace and clicking again returns it to where it came from. Multi-window apps keep cycling. The workspace name is the state, so a shell restart cannot strand a window: without a remembered origin it comes back to the current workspace. - Middle click opens a new window. - Reveal and tooltip dwell delays, so a pointer travelling to the bottom of a window no longer summons the dock and sweeping across icons no longer flashes every label. Tunable via revealDelay and tooltipDelay; set either to 0 for the old behaviour. Two rendering fixes: - Menu rows are drawn at one width. Each row used to size itself from its own label, so hover and checked fills stepped in and out down the menu (measured 232 / 225 / 273px in one menu), and a row changed width when it got checked. Rows now report an implicit width, the menu takes the widest, and every row draws at that. The mark column is always reserved, so labels share one left edge. - A custom dock colour no longer hides the dock's own chrome. bgColor is measured against the theme's bar foreground, and the launcher glyph, indicators, separator, and card outline flip to the readable side when the two land on the same side of the scale. A light theme with a dark card was drawing dark on dark at roughly 1.3:1. Internals: DockModel.cycleAppWindow and activateApp are replaced by a pure pickAppWindow that only chooses a window, leaving the acting to the host. That also fixes backwards wheel cycling on an unfocused app, which used to land on the second-to-last window instead of the last. The two duplicated tooltip blocks became one HoverTooltip component. Docs: new config keys, middle click, and the screen default, which the table described as the focused monitor while the code picks the first.
…ce & theme radius
… auto-contrast) into PR thepathless#2
…eliable multi-window minimize
Follow-up on the merge of PR thepathless#2. The features stay; these are the defects underneath them. Focus dispatch: - Dropped hl.dsp.workspace({ name = ... }). It does not exist — hl.dsp.workspace is a namespace (hl.dsp.workspace.toggle_special), so Hyprland 0.56 answers "attempt to call a table value (field 'workspace')" and the call was a silent no-op. Nothing was lost: hl.dsp.focus already switches to the window's workspace on its way. - The Wayland activate request is a fallback again instead of firing on every focus. It was landing before the Hyprland dispatch on every click, so each click asked for focus twice. - cycleApp no longer passes a second argument to focusToplevel, which has taken one parameter since the noWarp branch was removed. Remembered windows: - appLastActiveWindow held live Toplevel objects keyed by app, and was never pruned, so a closed window left a dangling handle behind. Both maps now hold addresses; addresses cannot dangle, and a stale one is simply dropped on the next prune. - Both maps were written by mutating the object in place and assigning it back to the same property. QML compares by reference, sees no change, and emits no signal. They go through DockModel.copyMap now. - lastPreDockActiveToplevel was written in six places and read in none, holding a live window handle for nothing. Removed; lastPreDockActiveApp carries what is actually used. - pruneMinimized became pruneWindowState: one pass over the live windows now trims every address-keyed map, not just the parked-origin one. Hyprland's own focusHistoryID would have replaced this bookkeeping entirely, but Quickshell only refreshes lastIpcObject when a window opens or closes, so the value is stale the moment focus moves — measured: the focused window kept reporting focusHistoryID 4 while a background window reported 0. Tooltips: - Visibility follows hover declaratively. Driving `shown` only from the containsMouse change signal leaves the bubble on screen whenever an item stops being hovered without that signal arriving — a model rebuild under the pointer, or the card sliding out from under it as the dock reveals. Seen in practice: a tooltip left hanging over an icon with the pointer at the screen edge. Hover and click feedback, per review notes: - The hover frame is gone. An icon grows and lifts on hover and dips on press; the plate, its border and the active-app fill were doing the same job three times over and the frame bounced along with the icon. Urgency, which does need to reach past the icon art, now rings the slot instead. - The launch bounce is tied to the app actually starting, which is what a dock bounce means: opening something that was closed, or middle-clicking a new window. Clicking an app that is already running moves focus and no longer throws its icon around. The fade stays as the launch cue when the bounce is switched off. - The apps button follows the same rule: it grows, it does not draw a frame. Co-authored-by: NothingManTR <NothingManTR@users.noreply.github.com>
Not chasing macOS materials — no liquid glass here. This takes the proportions and the reading order that a dock of that shape gets right. - The icon nearly fills its slot (iconSize - 4px instead of - 10px). With the hover plate gone there is nothing for the old inset to make room for, and the art is what the eye is looking for. - One round dot per running app. The second dot for "more than one window" was unreadable — two windows and seven looked identical — and the tooltip already lists them by name. - The card's corner radius tracks its own height for the "rounded" shape, so the panel keeps the same softness whether icons are 28px or 52px, instead of a fixed 14px that reads sharp at large sizes. Padding up one step, border a shade lighter. The nested ternary that computed the radius in three places is now one cardRadius() function. - The card casts a soft shadow, blurred in its own oversized layer because a blur inside the card's bounds gets clipped at the edge. It is bound to the card's own opacity so it leaves with it, and skipped entirely when the background is transparent, where a shadow with nothing above it looks wrong. Deliberately not done: neighbour falloff on hover, where icons either side of the pointer scale down with distance. That one needs the row to grow its spacing as it magnifies, otherwise neighbours overlap — a layout change rather than a visual one, and worth its own pass.
Hover used to be a binary 1.22x on the icon under the pointer. Now the whole neighbourhood responds, which is what makes a dock feel like one. The curve is the raised cosine Juan Pablo Zamora derived for this effect: size = min + ((1 - cos t) / 2) * (max - min) t = ((iconX - (cursorX - effectWidth / 2)) / effectWidth) * 2*pi, capped to [0, 2pi] which is 0.5 * (1 + cos(pi * d / R)) for a distance d and half-range R. It is flat at the peak and flat where the effect ends, so icons neither snap at the apex nor pop into motion at the edge of the range — the two artefacts a linear ramp is known for, and the reason his first attempt looked shaky. Measured against a linear ramp of the same range: identical slope at the peak, 24x gentler at the outer edge. Distances are taken from each slot's *unmagnified* centre, which the article is explicit about and which is the load-bearing detail. Slot widths never change, so growing an icon cannot move itself and alter its own distance to the cursor. Nothing can oscillate. The dock keeps its width; icons lean away from the pointer to make room. That matches how the widely copied dock recipes model it (fixed container, icons displaced, rather than a container that grows), and it avoids the same feedback problem from the other direction: a card that widens on hover slides its own contents under the cursor. - effect spans ~2.2 slots either side, peak 1.45x - icons scale from their bottom edge, so they grow up out of the dock and never cover the running dot underneath - the dot travels sideways with its icon but is not scaled or lifted - the tooltip clears the magnified icon - the apps button rides the same curve - 70ms easing: long enough to smooth the gaps between motion events, short enough that the icons still track the pointer Sources: - https://juankproblog.wordpress.com/2011/02/02/the-magnifying-effect-in-the-mac-os-x-dock/ - https://buildui.com/recipes/magnified-dock
This reverts commit 9f560d2.
Hover was a fixed 1.22x on the icon under the pointer. This adds the falloff across neighbours as an option and lets the old behaviour stay the default, because plenty of people prefer it. hoverEffect: "zoom" (default) | "wave" | "off" "zoom" is what the dock shipped with: only the hovered icon grows, the layout does not move. "wave" is the dock effect proper. "off" is no growth at all. One rendering path serves all three. The mode decides what drives the scale factor and whether a slot is allowed to grow; the icon is bottom-anchored in either case, so it rises out of the dock rather than spilling over the running dot, and the character of the animation is the same at either peak (1.22 vs 1.4). The old boolean is gone rather than kept alongside the mode — two spellings of one setting is the bug this codebase already had with clickToMinimize and minimizeMode. An existing magnification:false is read as "off" and the key is dropped on the next write. The wave itself: - Raised cosine falloff, the curve Juan Pablo Zamora derived for this effect: size = min + ((1 - cos t) / 2) * (max - min) across an effectWidth-wide window centred on the cursor, which is 0.5 * (1 + cos(pi * d / R)) for a distance d and half-range R. Flat at the peak and flat where the effect ends, so icons neither snap at the apex nor pop into motion at the edge of the range. - Slots grow and the row carries the extra width. This is not a stylistic choice. The displacement an icon needs is the accumulated growth between it and the cursor, which integrates to (peak - 1) * R / 2 — about 30px per side here. A fixed-width card has nowhere to put that, and nudging icons by a hand-picked amount instead leaves a hole next to the pointer while crowding everything further out. Measured on a first attempt at exactly that: the hovered slot's neighbour ended up displaced 15px while the hovered icon grew only 5px, a 10px gap where there should have been none. Letting the row grow is what keeps every gap even. Measured: the card goes 407px to 455px at the peak, against ~55px predicted by the integral. - Distances are measured from each slot's unmagnified home centre, in window coordinates, computed from the base row width and the window width. Nothing magnification changes feeds back into those numbers, so the wave cannot chase itself. - One animated factor drives both the slot width and the art width. Animating the slot alone is what made the first version look instant: the art was bound straight to the scale and snapped while the layout eased around it. Sources: - https://juankproblog.wordpress.com/2011/02/02/the-magnifying-effect-in-the-mac-os-x-dock/ - https://buildui.com/recipes/magnified-dock
The fade was written as the fallback for when the bounce was switched off, so turning the bounce on took it away. They belong together: the bounce says a click landed, the breathing says the app is still coming up. Kept shallower than the urgency pulse — opacity 1.0 to 0.61 against urgency's 1.0 to 0.35 — so "starting" does not read as "look at me". The two cycles are deliberately different lengths (740ms bounce, 650ms breath) rather than locked together.
buildEntries copied each window's `activated` flag into the model. The model is only rebuilt when a window opens or closes, so that copy froze on whoever held focus at that moment and never moved again. Visible as the tooltip marking the wrong window: with three terminal windows open, the accent dot sat on a window on another workspace while the focused one read as inactive. The flag is gone from the model. Focus is a single fact the manager already holds, so the four places that needed it now compare against ToplevelManager.activeToplevel directly — the tooltip's two colours, the minimize target, and the is-this-app-active test in activate(). All of them are live and cannot drift apart. pickAppWindow keeps reading `.activated`, which is correct there: it walks the manager's own toplevels, which are live objects rather than model rows.
The upstream indicator carried two facts: a pill that grew and took the accent colour when the app held focus, plus a second, smaller pill when the app had more than one window. Reducing it to one round dot dropped both, which left no way to tell a single window from four. Restoring the idea, minus the part that was not true: the old pair said "one or many", never how many. Two windows and nine drew identically. Now one dot per window, and the third widens into a pill once there are more than three, because past three nobody counts dots at 4px. Colour carries state (urgent, focused, parked), size carries focus (4px to 5px), which is the language the dock already spoke. The tooltip stops hiding windows too: it listed at most three with no hint that more existed, so it now shows four and says "+N more" for the rest.
The minimise-on-click behaviour that arrived with PR thepathless#2 had no rule for which window a click meant, and three defects fell out of that one gap. appParkedWindow held a single address per app, so parking a second window overwrote the first and restore could never find it again — four terminal windows with one parked, and that one had no way home through the icon at all. Parking ran before restoring, and parking one of several windows hands focus straight to a sibling, so the app never stopped being active: every further click parked another and nothing came back until all of them were gone. Putting restore first only moved the problem — the icon became a toggle on one window and a second could never be put away. The is-this-app-active test also consulted a remembered "app that held focus before the pointer reached the dock", a value that outlived what it described. So the behaviour is one ordered list now, decided from live state only — which windows exist, which are parked, whether focus is already inside the app. Nothing is remembered, so nothing can go stale: no windows launch it focus elsewhere, something parked bring the parked one back focus elsewhere focus it, preferring this workspace focus inside, mode "all" park the whole app focus inside, several open step to the app's next window focus inside, one open park it, when parking is on Two rules carry the weight. Preferring a window on the current workspace stops a click from teleporting you while the app is already in front of you. Stepping through windows is what makes every click on a multi-window app do something, and it is what breaks the deadlock above: with focus bouncing to a sibling, park-first parks forever and restore-first toggles forever, while stepping has no such corner. A specific window can still be parked from the context menu, which grows a "Minimize Window" row for exactly that, or the whole app in one click with mode "all". Both parking cases stay symmetric: "all" parks in one click and restores in one, "active" restores newest first. Supporting sets are small and read straight off the compositor — visibleWindows, parkedWindows, focusedIndex, windowHere, stepWindow — and the wheel steps the same set, so scrolling can no longer wake a parked window. Parked windows are found by the workspace they sit on rather than remembered, which is what makes losing one impossible.
Author
|
Huge thanks @assada for the incredible follow-up commits and thorough fixes! The live focus synchronization, deterministic click/minimize decision tree, multi-window dot indicators, and the raised-cosine dock wave effect take Omadock to a whole new level. 🚀 All your latest changes have been merged into this PR and tested live on Omarchy. Really appreciate the fantastic collaboration! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary of Changes
This PR includes both the Smart PWA/Flutter Icon & Animation System and integrates the improvements from PR #1 (Hyprland window focus, middle click, delays & auto-contrast) into a unified, conflict-free package.
🔍 1. Smart App & PWA Icon Matching (
DockModel.js&Dock.qml)getCandidates,isAppMatch) to accurately resolve:org.localsend.localsend_app->localsend.desktop).chrome-x.com__-Default,chrome-youtube.com__-Default->X.desktop,YouTube.desktop).omarchy-launch-webappdesktop entries.🎨 2. Visuals & Animation Enhancements (
Dock.qml)⚙️ 3. Theme Radius & Settings Integration
Auto (Theme)dock shape option dynamically syncing with Omarchy / HyprlandStyle.cornerRadius.Magnification (Zoom),Launch Bounce,Window Previews,Urgent Highlights,Click Active to Minimize, andAuto (Theme) Shape.🪟 4. Hyprland Integration & UX (from PR #1)
revealDelay/tooltipDelay).