* fix(bar): keep widget content on whole pixels
Bar widgets centre their content inside a box of
widgetThickness - horizontalPadding * 2, and Qt rounds the offset of a
centred anchor to whole pixels. When the content and that box have
different parity the exact offset falls on a half pixel, Qt rounds it
away from zero and the content renders half a pixel above the centre.
Widgets drawn from rectangles, such as the workspace switcher, compute
their own snapped height and stay exactly centred, which is why they
look right next to everything else in #3074.
With the stock configuration this is the normal case: the bar is 40 px,
the box is 14 px and barIconSize returns round((40 / 48) * 18) = 15.
Two sizes now round to whole pairs of pixels. The icon size, so that it
matches the parity of the box, and widgetThickness, because the box
inherits its parity from there (the padding is doubled, so it never
changes it). Going through every innerPadding from 0 to 16, nine of the
seventeen configurations currently put the icon on a half pixel and none
of them do afterwards. The 48 px default bar is untouched, the sizes
that change do so by one pixel.
Measured in a nested Hyprland at scale 1, ink centroid of the widget
content relative to the centre of its pill, before and after:
launcher -0.50 +0.00
weather -0.81 -0.52
clipboard -1.49 -0.93
notifications +0.03 +0.46
battery -0.63 -0.49
bluetooth -0.53 -0.03
Every icon moves down by half a pixel. What remains matches the ink
asymmetry of the individual glyphs, measured separately at an exact
position, and is not a placement error.
Two things this does not cover. On a fractional or HiDPI scale
horizontalPadding can land on a half pixel, which puts the box back
between the grid lines, and a text only widget such as the clock has a
line box of 15 px at the default font size, which cannot sit exactly on
the centre of a 28 px pill no matter how it is rounded.
Closes#3074
* fix(bar): give bar text a line box that can be centred
A widget without an icon is centred as one line box, and the same parity
rule applies to it as to the icon in the commit before this one. At
fontSizeSmall the line box of the bar font is 15 px tall, so on a 28 px
pill it cannot sit on the centre no matter which way the offset is
rounded, and the clock reads about half a pixel high next to everything
else. One step up the box is 16 px and it lands on the grid.
Only the branch the standard bar uses changes. The 0.9 branch below a
scale of 0.75 already comes out at 14 px, and the fontSizeMedium branch
above 1.25 is left alone because there the next even box is two steps
away, which would be a bigger change than this is worth.
Same session and same measurement as the previous commit, ink centroid
of the widget content relative to the centre of its pill:
clock and date -0.58 +0.05
battery -0.63 -0.05
weather -0.82 -0.28
The bar font goes from 12 px to 13 px, which is the visible part of this
and the reason it is a separate commit. It can be dropped without
touching the icon fix.
* bar: snap widget thickness to even device pixels and pick text sizes by line box parity
* bar: snap bar thickness to even so the pill centres on whole pixels
* bar: share the bar thickness formula so dock, osd and popouts stay in step
* dock: read the bar clearance at the compositor screen scale
---------
Co-authored-by: bbedward <[email protected]>
* feat(theme): add a selectable source color mode for wallpaper palettes
DMS always seeds the palette from matugen's most dominant candidate
(--source-color-index 0, hardcoded at both call sites). When a large
muted region outvotes a smaller vivid one, a visibly warm wallpaper
produces a cold palette: one image here goes to #8ad0ee where the warm
subject is at hue 46.
Adds a Source Color setting with seven values. Dominant is the default
and is byte-identical to today. Colorful is computed in Go before
matugen runs: quantize the image (Wu for the starting clusters, then a
weighted k-means in Lab), score the result the way caelestia-cli does,
and hand matugen the resulting hex. Its scoring walks a chroma/tone bar
down from 20 until something clears it, which biases the pick toward a
colorful, well-lit color rather than whichever muted color covers the
most pixels. The remaining five map to matugen's --prefer.
- core/internal/matugen/sourcecolor.go: extraction, scoring, and the
one helper both matugen call sites now share
- buildOnce resolves the seed once, before the StockColors branch, so
the dry run and the real run cannot disagree about it
- extraction failure logs a warning and falls through to matugen's own
extraction; it never fails a theme build
- --prefer arrived in matugen 4.1, so it is gated separately from the
existing isV4 check; on 4.0.x those five modes degrade to the
dominant color instead of aborting on an unknown argument
- --source-mode on generate and queue, plus sourceMode on the socket
handler
- Source Color dropdown in Theme & Colors, defaulting to Dominant
Adds github.com/Nadim147c/material/v3 (Apache-2.0, no dependencies) for
HCT conversions, Wu quantization and the dislike analyzer. Its
QuantizeCelebi is not used: it returns after one unconverged iteration
and its Lab.DistanceSquared is a dot product rather than a distance, so
it collapses most of an image into a single cluster.
* fix(theme): address review feedback on source color mode
Two points from review on #3020.
The quantize comment pointed at a FOLLOWUPS file that does not exist in
this repository. Replaced the reference with the measurement it was
standing in for, so the claim is self-contained.
setMatugenSourceMode called Theme.generateSystemThemesFromCurrentTheme()
directly on top of the regenSystemThemes onChange hook that Spec.set
already dispatches. Both feed a 100ms debounce so it was one generation
either way, but the direct call was redundant and is gone.
* fix(theme): keep {{image}} resolvable in colorful mode
Colorful resolves the seed itself and calls matugen with a hex source, which
leaves matugen's {{image}} unset: the builtin pywalfox template and any user
template using the keyword render the literal string "Null".
Pass the wallpaper path alongside dank16 in --import-json-string, as suggested
in review, so those templates keep working unedited. The path is made absolute
to match what matugen renders for {{image}} in the other modes.
* fix(theme): cut translator terms on the source color dropdown
Per-mode descriptions were seven of the fifteen new terms this branch adds,
and five of them differed by a single adjective. Drop them: the dropdown row
already describes what the setting does, and the docs cover each mode.
The battery popout already surfaces the UPower energy rate, but the bar
widget could only show percentage and remaining time, so telling how fast
the battery is charging or draining meant opening the popout (or running a
third-party plugin that polls /sys/class/power_supply).
Add two independent, opt-in toggles to the battery widget - "Show Charge
Rate" and "Show Discharge Rate" - that render the signed rate next to the
existing text, e.g. "+28W" while charging and "77% (2h 11m) -19W" while
draining. Both default to off, and follow the established pattern: a global
default in SettingsData plus a per-widget-instance override.
The value comes from BatteryService.changeRate, so there is no extra process
or polling. It hides itself when the rate is below 0.05W (idle, fully
charged, or sitting at a charge limit) so it costs no bar space unless it
has something to say. Precision adapts to keep the pill narrow: one decimal
below 10W, whole watts above; vertical bars get a whole-watt line.
* fix(dock): hide overlay dock over fullscreen
* fix(dock): track fullscreen state declaratively
* fix(dock): clear overlay mask during fullscreen
* feat(dock): add fullscreen visibility option
* fix(dock): preserve auto-hide fullscreen behavior
* fix(dock): keep fullscreen mask interactive in overview
* fix(frame): host dock on overview without show dock
* feat(mpris): pin preferred media source
Add a minimal "pin media source" option so the bar media widget and OSD
keep showing a preferred player (e.g. a music player) even when another
source (e.g. YouTube in the browser) starts playing.
- MprisController: new pinnedIdentity property + setPinned(identity);
_bestPlayingPlayer() prefers a playing pinned player (exact identity
match, then case-insensitive substring fallback). Existing behavior
is unchanged when no pin is set.
- Persistence via SessionData.pinnedPlayerIdentity (spec entry added so
SessionData.set() accepts the key); empty string clears the pin.
- MediaDropdownOverlay (Media Players panel): each player row gets a
theme-colored circle indicator on the right. Empty circle = no pin
(initial state). Right-click a row to make it the default source
(circle fills); right-click another row to move the pin; right-click
the pinned row again to clear (back to empty). Left-click behavior
unchanged (selects the player).
* Update quickshell/Services/MprisController.qml
Concur with Claude's suggestion.
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* Update quickshell/Services/MprisController.qml
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* feat(theme): add support for matugen smart mode and type
* add another dry run to get smart type preview
* fix too much keywords in search index
* revert dont extract translations
* feat gate "Auto From Wallpaper" toggle row and the available matugen scheme for the pallete drop down
* re-extract settings search index
* make portal sync no longer disables matugen smart mode
* make sure cosmic theme mode also follow change from smart mode
* add translator context for Auto (Light Mode) / Auto (Dark Mode) / Auto From Wallpaper
* fix: gate matugen smart mode checks on the dynamic theme so stock themes keep portal sync and fix tile labels in control center
Adds a user-configurable spacing between system tray icons in the bar,
available per widget via the tray context menu in Settings > Widgets
("Icon Spacing" stepper, 0-20px), with a global default fallback.
- SettingsData/SettingsSpec: new trayIconSpacing setting (default 0)
- SystemTrayBar: apply configuredIconSpacing to the horizontal Row and
vertical Column layouts, and account for it in auto-overflow item
estimation and drag-and-drop slot math (shift offsets and target
index calculation)
- WidgetsTabSection: "Icon Spacing" stepper in the tray widget context
menu, following the existing "Max Visible" pattern
- WidgetsTab: keep trayIconSpacing when restoring per-widget data
* fix(processlist): stop polling when the popout is closed
The popout's content loader deliberately stays warm across close so
re-opening is instant, and PopoutService only defers the actual unload
until session lock or monitors-off. But both DgopService references were
bound to component lifetime rather than presentation, so after closing
the popout refCount stayed above zero: dgop kept sampling the full
process list every 3s until the shell restarted or the session was
locked and unlocked.
Gate both references on shouldBeVisible instead. Ref grows an `active`
property (defaulting to true, so every other call site is unchanged) and
ProcessesView grows an `active` property that hosts keeping it loaded
while hidden can drive. ProcessListModal is unaffected — its tab loaders
already destroy the view when the window hides.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GTGf29PYTp1kpAv5HNHBzM
* Update quickshell/Modules/ProcessList/ProcessListPopout.qml
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: BB <[email protected]>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>