Skip to content

feat(media): add basic implementation for progress fill on media widget - #3818

Merged
ItsLemmy merged 13 commits into
noctalia-dev:mainfrom
GhostEagle68:feat/media-progress-fill
Aug 25, 2026
Merged

feat(media): add basic implementation for progress fill on media widget#3818
ItsLemmy merged 13 commits into
noctalia-dev:mainfrom
GhostEagle68:feat/media-progress-fill

Conversation

@GhostEagle68

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in show_progress option to the bar's media widget. When enabled, the widget's
background fills left-to-right in proportion to the current track's playback position, so the pill
itself doubles as a progress indicator.

The fill is a ProgressBar child of the widget's own root, sized to the widget content and widened
by the capsule padding so it spans the pill. It is drawn at zIndex(-1), opted out of layout, and
not hit-testable, so it changes nothing about the widget's measured size or input behavior.

Defaults to off and hidden on vertical bars and in album-art-only mode, where a rectangular fill behind
a circular album-art disc doesn't read well.

Motivation

The media widget shows what's playing but gives no sense of where you are in the track. Getting
that today means opening the control center. A background fill answers it without
consuming any extra bar width.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Build / packaging

Related Issue

None — this wasn't tracked by an existing issue.

Testing

just build      # clean, no new warnings
just test       # 72/72 pass
just format     # clang-format, no diff
python3 tools/i18n-check.py    # OK: 867 tr keys, all present in catalog

Manual testing against a live debug shell with Spotify and browser MPRIS sources:

  • Fill advances smoothly during playback; title stays legible over it.
  • Paused: fill freezes and the update timer stops — verified via /proc/<pid>/stat sampling that no
    additional wakeups occur while paused.
  • Seeking from the control center: the bar fill jumps to match.
  • Live stream / unknown duration (lengthUs == 0): fill hidden, no divide-by-zero, no full bar.
  • Option off: no fill, no timer.
  • Widget capsule enabled and disabled: fill geometry correct in both (the capsule-padding
    extension is gated on shouldShowBarCapsule()).
  • Vertical bar: fill hidden, setting hidden, existing art-only rendering unchanged.

Manual Coverage

  • Tested on Niri
  • Tested on Hyprland
  • Tested on Sway
  • Tested on another compositor:
  • Tested with different bar positions and density settings
  • Tested at different interface scaling values
  • Tested with multiple monitors

Screenshots / Videos

image image

With capsule
image

Checklist

  • This PR is ready for review, or it is marked as Draft.
  • I read and followed the relevant guidance in CONTRIBUTING.md.
  • I ran just format with clang-format v22+ installed, or this PR has no code changes.
  • I ran the relevant build or test commands, or explained why they were not run.
  • I self-reviewed the changes.
  • I checked for new warnings or errors.
  • I will update end-user documentation after merge, or this PR does not change user-facing configuration or behavior.
  • I added or updated assets/translations/en.json, or this PR adds no new user-facing strings.
  • I did not edit non-English translation files unless this PR is explicitly for translation tooling, an import/export sync, or a maintainer-requested locale change.
  • I used the existing canonical names for config keys, IPC names, paths, and identifiers.

Additional Notes

Why a timer instead of a frame tick: The fill only moves in whole pixels, and the widget is
80–220 px wide, so one pixel of travel takes trackLength / widgetWidth. Roughly 0.9 s for a
3-minute track in a 200 px widget. Driving this from needsFrameTick() would redraw ~60x more often
to produce an identical image. Instead syncProgress() arms a one-shot Timer with

intervalMs = clamp(trackLengthMs / fillWidthPx, 250, 1000)

and re-arms it from its own callback, so the cadence tracks the current track length and widget
width. It is armed only while playbackStatus == "Playing" with a known duration, and stop()s on
pause, stop, and when the option is off. A paused track costs zero wakeups.

Reused existing primitives, no new ones.: ProgressBar (src/ui/controls/progress_bar.h)
already renders a full-size fill revealed through a clip node, which keeps the leading end's rounded
cap instead of squaring off at low progress. Position comes from MprisService::activePlayer(),
which already returns a wall-clock-projected snapshot, so there's no extra D-Bus traffic.

Fill color: routes through widgetForegroundOr(...) at 25% opacity, matching how
battery_widget.cpp and sysmon_widget.cpp tint their fills, so a [widget.media] color override
applies to the fill as well as the text. Because ColorSpec stores a role rather than a resolved
color, theme switches are picked up automatically by ProgressBar's paletteChanged() connection.

No customization options: I wanted this PR as a base for the function and further customization options would be better suited for a new PR.

@GhostEagle68 GhostEagle68 changed the title Feat(media) progress fill on media widget feat(media): add progress fill on media widget Aug 6, 2026
@AdityaKr015

Copy link
Copy Markdown
Contributor

Nice one

@GhostEagle68 GhostEagle68 changed the title feat(media): add progress fill on media widget feat(media): add basic implementation for progress fill on media widget Aug 10, 2026
@ItsLemmy

Copy link
Copy Markdown
Collaborator

[P2] Timer callback mutates the scene outside a UI phase

src/shell/bar/widgets/media_widget.cpp:262

The one-shot timer invokes syncProgress() directly. Timers run from TimerPollSource::dispatch(), outside PrepareFrame, Update, or Layout. syncProgress() then calls ProgressBar::setProgress(), which immediately changes the retained clip geometry.

This bypasses the repository’s phase contract: callbacks should schedule work; state synchronization belongs in Update. It also samples m_progressBar->width() while a layout may be pending.

Have the timer callback call requestUpdate() only. Sample MPRIS state, apply progress, and rearm the timer from doUpdate().

[P2] Capsule padding math is invalid for capsule groups

src/shell/bar/widgets/media_widget.cpp:200-205

shouldShowBarCapsule() is true for both single-widget and grouped capsules, but the code always expands the media fill by one capsule padding on each side. Group padding belongs around the entire group, not every member.

Because widget roots do not clip children, a supported configuration such as capsule_padding = 6 and group widget_spacing = 2 makes the fill overlap the next widget by 4 px. Accordion groups have the opposite failure: their dedicated clip excludes capsule padding, so the expanded fill cannot reach the capsule edges. The fill also uses rootNode->height() rather than the bar-owned capsuleCross, so custom
capsule thickness cannot be represented correctly.

The fill needs bar-owned capsule/member bounds, or it must remain confined to the widget root until a group-aware overlay API exists.
Verify single capsules, multi-widget groups with spacing smaller than padding, both accordion directions, and non-default capsule_thickness.

@ItsLemmy
ItsLemmy marked this pull request as draft August 15, 2026 05:04
@GhostEagle68
GhostEagle68 force-pushed the feat/media-progress-fill branch from 304d869 to 9fd9ea3 Compare August 19, 2026 17:47
@GhostEagle68
GhostEagle68 marked this pull request as ready for review August 19, 2026 18:30
Copilot AI lite review requested due to automatic review settings August 19, 2026 18:30
@GhostEagle68

Copy link
Copy Markdown
Contributor Author

Grouped
image
Group+Spacer
image
Widget at end
image
Spacer Length Zero
image
Outside Group
image
Non default capsule padding; 28
image
Accordion
https://github.com/user-attachments/assets/0682af72-019e-46ea-9d24-c1b8c64f4e51

@ItsLemmy here are some testing validations 😄

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in playback-progress background fill to the bar media widget, using an underlaid ProgressBar plus a timer-driven update cadence to avoid per-frame ticking.

Changes:

  • Add show_progress option for the media widget (hidden for vertical bars and album-art-only mode).
  • Render a non-layout, non-hit-testable ProgressBar behind the widget content and sync it to MPRIS position/duration.
  • Add an English translation entry for the new setting.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/shell/bar/widgets/media_widget.h Adds showProgress option, ProgressBar pointer, and timer member; declares syncProgress().
src/shell/bar/widgets/media_widget.cpp Builds and lays out the progress fill; updates progress via syncProgress() and a one-shot Timer.
src/shell/bar/widgets/media_widget_definition.cpp Exposes show_progress setting and constrains it to horizontal/non-art-only cases.
assets/translations/en.json Adds label/description strings for the new setting.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/shell/bar/widgets/media_widget.cpp Outdated
area->addChild(
ui::progressBar(
{.out = &m_progressBar,
.fill = withOpacity(widgetForegroundOr(colorSpecFromRole(ColorRole::Primary)), 0.25F),
@ItsLemmy

Copy link
Copy Markdown
Collaborator

Thanks

@ItsLemmy
ItsLemmy merged commit 3008338 into noctalia-dev:main Aug 25, 2026
2 checks passed
@GhostEagle68
GhostEagle68 deleted the feat/media-progress-fill branch August 25, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants