Repository navigation
feat(fit): the dock always fits its screen - #64
Merged
Merged
Conversation
Durations had drifted across the dock (120 ms linear next to 120 ms OutCubic, 140, 150, 160, 180) and a setting that slows or stops motion needs a single switch. DockMotion.plan(level) gives the timings and flags for Full, Reduced and Off; the next commits make the dock read it.
It only edits state, like the slot logic beside it; moving it frees the root's lines for the animation setting that follows.
Motion & Effects gets one switch for how the dock moves. The value is a behaviour preference, so it lives in omadock.json but not in presets; Dock.motion carries the plan the components read.
A key dropped from a section stays in the model, marked leaving, for the length of its exit, and keeps its place; a key that comes back in that time revives instead of being rebuilt. Each row also carries its slot among the live rows, which label slots and home centres keep using while something leaves. Row keys for tiles, folders, buttons and drives come from the same module.
The model now applies DockKeys rows: removals wait out exitMs (the motion plan's base duration, 0 at Off) and every row carries its live slot. The delegates pick the roles up in the next commits.
Folders, command buttons, drives and their dividers move verbatim into DockRightGroup, which gets the card, its wrapper and the outer row passed in. DockCard was at its 800-line cap and the row animations that follow need room; the probe's item geometry is identical before and after.
…the row Each element of the pinned run, the running run and the Omarchy button sits in a RowSlot, which narrows and widens with the element and scales and fades it (Reduced: fade only, Off: at once). The model keeps a closed app's row for its exit, so the card and the neighbours ease along instead of snapping; label slots and home centres use the live slot, and the drag logic and itemGeometry measure live rows only (liveSlots). A fully tiled app leaves the running run the same way.
A delegate's model roles reach it after it completes and without a change signal, and a required property that shares a name with one of RowSlot's own made a second property RowSlot never read, so nothing animated out. Delegates now mark RowSlot's own leaving and fresh as required; the slot draws as nothing for one turn, then grows if it is fresh. Rows that arrive while the dock starts (the first 1.5 s of the model) are not fresh, so startup does not animate in.
Minimized tiles, the folder/button run and the drives were plain arrays rebuilt on every change; they move to KeyedListModel (DockKeys.tileKey, folderKey, driveKey) inside RowSlots like the app runs. Every keyed delegate now takes its data from the lookup only while the key is there (a Binding read the gap first, so a leaving folder lost its name and narrowed from the wrong width), and the trailing drop gap and ghost count live folders.
The four divider slots become one RowDivider: its width eases to nothing and its line fades when its section goes, and it stays in the row while it has width, so the split panel after it narrows with the items. The right group stays visible while its rows are on their way out. The divider sets its direction before its width changes, so the leaving curve matches the items'.
The queue bookkeeping moves beside dropParkSlot and gets the dock's timer passed in; the root keeps the Timer and a one-line delegator, which frees its lines for the popup lives that follow.
The context menu, the folder stack and the opened group were destroyed the moment they closed, and their opacity Behaviors never ran. Each now has a PopupLife in the dock that keeps its window until the fade ends; the window fades and rises in, and on close shows the body's last frame (a ShaderEffectSource kept current while open), since the popup's own state clears at once. No input and no focus grab while it fades out.
A window mark popped in at full size and vanished on the spot, and the badge flipped visible. A mark that goes now stays one exit long while it shrinks and fades, a new one grows from nothing, and the badge scales and fades in and out with the last count shown while it fades; its width eases when the count gains a digit.
Behaviors and transitions read Dock.motion (through each component's root, or through hoverFx for HoverFx and DockIconArt): the magnifier and press dip keep their 110 / 90 ms, fades and colours take fast, layout and hover levels base, the reveal and the autohide slide slow, with OutCubic in place of the stray linear and OutQuad fades. Autohide slides only at Full and fades at Full and Reduced. A static test lists any literal duration left; the bounce, pulse, halo, ripple, glitch and label typing keep their own timings and are listed as such.
The launch and urgent bounce, the pulse, the drop-target halo, the glow ripple, the glitch burst and the label typing and scrambling run only at Full. The bounce and the pulse now finish the hop or breath in flight before they rest (alwaysRunToEnd), so a window appearing mid-bounce no longer snaps the icon down from 13 px.
It draws its line in Color.bar.text, so like the other files that use Color it needs the versioned import (Qt 6.12's own Color singleton).
The folder icons moved out with the right group (DockRightGroup), which imports it itself.
The menu, the folder stack and the opened group size themselves to 0 the moment their state clears, and a ShaderEffectSource whose source is 0x0 draws nothing: the fade-out ran on an empty window. While closing, the body keeps its last size.
Two identical command buttons are one button (DockFolderLogic); in the keyed model they made two rows, and unpinning one left the second row in place for good.
A leaving row shared its slot with the live row after it, so a drag opened two drop gaps (one in the leaving item), a file drag found the leaving folder's gap, and a row that came back before its exit ended had lost its label width entry to its neighbour for good, skewing home centres and plate edges. Leaving rows now report label slot -1 and take no drop line or folder gap; reviving re-registers the label.
The slide was gated on layout motion, which Reduced turns off, so the card left in one frame while its opacity faded out of view. At Reduced the card now moves off screen once the fade ends, and back before it fades in.
The dividers and the Omarchy button follow data that arrives after the dock is built (the pinned list, the first window list, the config), so every shell start grew the dividers in and a hidden Omarchy button narrowed out. Changes in their first 1.5 s land at once; the divider's width is set by hand after its direction, like RowSlot's appear.
An easing bound to the same flag as the animated value (want, count, wanted, going, dockVisible) could start with the old easing: on the way out the popups, badges, row items, window marks and the autohide fade ran OutCubic instead of InCubic. The Reduced autohide pause had the same race, so the card could leave the screen before its fade. Each Behavior's targetValue is set before its animation starts, so the easing and the pause read the direction from it.
PopupLife ends a popup's life when its level gets back to 0. Opened and closed in one turn, the level never left 0, no change came, and alive stayed true: the popup's window (its LazyLoader) lived on, invisible. Closing now ends the life at once when the level is still at 0.
…rinks Every plan change restarted the timer that removes a going mark, so with frequent changes (a window title, the focus) the mark lingered past its exit. The timer now starts once per exit. The mark's cell kept its full size while the mark shrank and faded, and the others jumped when it was removed; along the row the cell now grows and shrinks with its mark (Full only), with the mark centred in it.
During its 180 ms exit a leaving app, folder or drive could still be clicked, hovered or dropped on. RowSlot disables its content while it is not wanted.
RowDivider stays visible while it has width, so a split panel narrows instead of vanishing. inPinZone and overRunningAt read that visibility, so for the length of the exit a going divider still decided where the pin zone and the running apps started. They now also check that it is shown.
Reduced keeps fades and drops movement, but tooltips still lifted 6 px as they faded in. TooltipLife exposes the motion plan's rise flag and TooltipWindow lifts only with it.
It read only Dock.qml and components/*.qml, so a literal duration in components/logic or components/settings went unseen. It now scans every QML file outside tests, allows the settings search-hit highlight (not the dock), and fails on an allowance that no longer matches a line.
With always-on labels a row of pinned apps spends most of its width on names of apps that are not open. labelPinned "running" (Settings -> Labels -> Pinned apps, shown for always-on labels on apps) folds a pinned app's name away while it has no window: the icon stays, on its own plate in a plate row, and a hover shows the tooltip with the name. When the app starts, the name waits for the launch bounce: the hop in the air when the window appears lands first (the animation's running turns false at once under alwaysRunToEnd, so the tile reads the icon's height and a ScriptAction after the fall), then the plate widens. Full eases the width, Reduced snaps it and fades the name, Off is instant. Closing the last window folds it again.
With side indicators on plates, a pinned app folded by "When running" lost its faint "playing without a window" mark: the column's room and opacity scaled with the name's progress, which is 0 while folded, and the mark under the icon is hidden whenever side marks are on. A foldable name now keeps the column's room and opacity on their own: before the icon as the usual lead, after it as a tail that hands over to the name's trail as the name opens. While folded the column holds only the background mark; window marks still wait for the bounce to land.
A right click on an app in an opened group showed the app's menu for a moment and then dismissed it. The group closes as the menu opens, and with animations its window stays mapped for the fade-out; when it unmapped about 200 ms later, Hyprland cleared the menu's focus grab and the menu took that as a click outside. With Animations Off the group window went at once and the menu stayed, which pinned it down. Each popup life now knows its rivals (folder stack, opened group, menu). One that opens cuts a rival still closing: level to 0 without the Behavior and the window let go, so nothing unmaps under the new popup's grab. The fade is lost only where another popup takes its place.
Pure cascade: shorter labels, folded labels (closed pinned, apps, all), spacing, icons down to a floor, then overflow from the end of the row. Hysteresis keeps a gentler plan out until it fits with a margin.
The fit planner needs each name's unshortened width and the room around it, taken from targets so the report cannot depend on the plan it feeds. The registry lives in the new DockFitHost: Dock.qml has no room left under its line cap.
…hrinks icons to fit DockFitHost feeds the planner from the dock's own lists and the labels' full widths and holds the plan; Dock.iconSize, gapWidth and sectionGap read it, labels take its max width and folds. Every always-on name is foldable now, so a name the plan folds keeps its plate like a sleeping pinned app's.
The planner's cut hides items from the end of the row through their RowSlots, so they leave with the usual exit; dividers, plate edges and the Both sides shift count only what stays.
A popup like an opened group lists the overflowed items in dock order; a click acts as on the dock and a right click opens the item's own menu. Its life is a rival of the stack, group and menu lives, so hand-overs keep the new popup's focus grab.
The app library's icon for a web app or a terminal app is not the one its dock tile shows (entry.icon); the popup should look like the dock it stands in for.
…dges A notification or an urgent app in the overflow would otherwise be invisible.
With indicators beside the art on plates, a folded name kept only the background-media column: right for a pinned app that sleeps or is still starting (When running), wrong for a running app the fit folded, whose windows then showed nowhere. The tile now says why the name is folded, and the width report counts the column.
… item left Drop zones must not move under the pointer; a stack or group anchored on an item that moved into » has nothing left to point at. README: the dock always fits its screen.
A delegate the fit hides leaves the row and drops its label report; planning it at 0 let it back in, where it reported again and no longer fit, forever (DockFit.retainLabels keeps the last report). Both sides' gap item is always in the row, one more spacing and a one-pixel floor the plan did not count. With overflow the right group's shift reads the packed resting width (plan.rest), not the prediction with its Wave reserve.
The rival cut only covered one order of a hand-over: the old popup already fading when the new one opens. When the new popup's open condition changed first (a folder clicked in a popup that closes on that change), the old one was not closing yet, so nothing was cut; it then faded, unmapped and cleared the new popup's focus grab, which dismissed it. A life that starts closing while a rival wants to be open now ends without the fade. Checked offscreen with two lives in both orders, and that a plain close still fades.
priard
added a commit
to priard/omadock
that referenced
this pull request
Oct 11, 2026
Owner
|
🎉 Thank you so much for this contribution, @priard! Really appreciate the care you put into it. ✨ Verified on experimental — static gates, unit suites (239 node + 116 python) and the full live dock battery all green, including the marks-centering suite — and merged. Your work is credited in the changelog. 🙏🚀 |
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.
Problem
Nothing limits the dock's width. A floating dock grows past the screen edges, and in Panel or Both sides a row wider than the card runs past the right edge. With enough pinned apps, running apps, minimized tiles, folders and drives, items end up off screen where they cannot be reached.
What it does
The dock always fits its screen. When the row would be wider than its room, it gives width back in a fixed order, least painful first:
max(24, size / 2).There is no setting. The "»" tile carries what the hidden items would show: a mark when one has windows, their urgency and their notification badges.
How
DockFit.jsis a pure planner, tested in node likeDockLayout.js. It takes the row's items (kind, pinned/running, each label's full unshortened width) and the sizes, and returns one cascade state: label max width, fold level, spacing fraction, icon size and overflow cut.components/DockFitHost.qmlgathers the inputs from the dock's own lists and the labels' width reports. It recomputes the plan a turn later (child Timer), holds the hidden item ids and the shown counts, and owns the "»" popup's life and window.Dock.qmlis at its line cap, so it only readsiconSize,gapWidthandsectionGapfrom the plan. Components reach the host throughroot.fitHostRef.DockFit.retainLabels); planning it at 0 would let it back in, where it no longer fits, forever.RowSlots, with the usual exit. Dividers, plate edges and the Both sides shift count only what stays.Two fixes to #59's popup rivals
PopupLife.rivals.PopupLifes.Depends on #59 and #63
This is cut from
feat/pinned-labels(#63, which is cut from #59). The diff includes both until they merge. The commits of this PR are the ones after #63's, from "feat(fit): a planner that keeps the row inside its room" on. I'll rebase ontoexperimentalonce those are in.Testing
tests/unit/fit.test.mjs(19 tests):CI-equivalent checks pass locally, including
structure-check.py(Dock.qml2410 of 2412).Live, with a temporary IPC override of the room:
The journal stayed clean.
Tested by hand: the "»" popup, clicking folders and apps in it, menus from it.
Known minors, left for later
gapsOutchanges do not trigger a replan until something else changes.