Skip to content

feat(ui): redesign the desktop screen share picker - #1342

Draft
renefloor wants to merge 7 commits into
v2from
feat/improve-desktop-screen-share
Draft

feat(ui): redesign the desktop screen share picker#1342
renefloor wants to merge 7 commits into
v2from
feat/improve-desktop-screen-share

Conversation

@renefloor

Copy link
Copy Markdown
Contributor

Redesigns the desktop screen-share picker onto the design system, and stops it hammering the platform while it is open.

Figma: Screen Share Modal

Why

The old picker was a raw Material AlertDialog with a Material TabBar, styled from the pre-design-system StreamVideoTheme.colorTheme, with a hard-coded 640×560 body and Colors.blueAccent selection borders.

It was also slow. ScreenSelectorStateNotifier ran a Timer.periodic(2s) calling updateSources() for as long as the dialog was open. Every tick made the platform re-enumerate every screen and window and re-capture a full-resolution bitmap of each — getSources was called without a thumbnailSize. Each thumbnail event then allocated a new state object, which rebuilt the entire dialog through one top-level ValueListenableBuilder, and called setState on the tile, re-decoding an uncapped Image.memory on the UI isolate. With ~30 windows open that is 30 full-tree rebuilds and 30 full-res decodes every two seconds.

On top of that, the notifier was disposed only in the Cancel and Share handlers. Dismissing with Esc or a tap outside leaked the notifier, its three subscriptions and the timer — which then kept re-enumerating sources for the rest of the app's life, stacking one more per open.

What changed

Loading. ScreenShareSourceController replaces ScreenSelectorStateNotifier. It reads both source types in a single getSources call when the picker opens, and again only when the refresh button in the header is pressed. No timer, no onAdded/onRemoved/onThumbnailChanged subscriptions. It asks for 480×300 thumbnails instead of whatever the platform defaults to. Switching tabs is now a filter over already-loaded state rather than a wipe-and-refetch. The dialog owns the controller through a StatefulWidget, so it is disposed whichever way the dialog goes away.

Design. StreamModalDialog + showStreamModalDialog (header with title and actions, footer, StreamBlurScrim barrier) and StreamTabBar join src/widgets/design_system_candidates/stream_core_flutter ships none of them yet. StreamScreenShareSelector is the grid, StreamScreenShareThumbnail one tile, and StreamScreenShareSelectorThemeData restyles them.

A tile's selection border paints as a foregroundDecoration, so going from the unselected 1px to the selected 2px never resizes the tile.

Breaking

TabbedScreenSelectWidget, ThumbnailGrid, ScreenSelectorStateNotifier and ScreenSelectorState are gone; ScreenShareThumbnailWidget is StreamScreenShareThumbnail. showDefaultScreenSelectionDialog and StreamScreenShareButton.desktopScreenSelectorBuilder keep their signatures, so the common path needs no change. The dogfooding custom-selector example is ported.

Tests

  • ScreenShareSourceController: one getSources for both types, a capped thumbnailSize, updateSources never called, a tab switch that reloads nothing, refresh that reloads once, a dropped selection, a failed load.
  • StreamScreenShareSelector and showDefaultScreenSelectionDialog: tab contents, selection, the empty state, no polling over ten seconds, and picking a source through the real entry point.
  • StreamTabBar and StreamModalDialog: indicator, tap reporting, header and footer slots, close button, dismissal through the scrim.
  • Goldens for the whole modal and for both tabs, in light and dark. Snapshotted inline rather than through the route — the CI capture path drops Overlay content.

🤖 Generated with Claude Code

renefloor and others added 2 commits September 11, 2026 14:11
Rebuilds the picker on the design system's modal dialog, and stops it
re-enumerating the platform's screens and windows every two seconds.

The old picker ran a `Timer.periodic(2s)` calling `updateSources()` for
as long as it was open, which made the platform re-capture a
full-resolution bitmap of every screen and window, rebuild the whole
dialog per thumbnail event, and re-decode each image on the UI isolate.
The timer also outlived a dismissal by escape or barrier tap, so it kept
running for the rest of the app's life, one more per open.

`ScreenShareSourceController` reads both source types in a single call
when it opens, and again only when the refresh button is pressed. It
asks for 480x300 thumbnails rather than whatever the platform defaults
to, holds no timers and no stream subscriptions, and is disposed
whichever way the dialog goes away.

`StreamModalDialog`, `StreamBlurScrim` and `StreamTabBar` join the
design-system candidates; `StreamScreenShareSelector` is the grid, with
`StreamScreenShareSelectorThemeData` to restyle it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

renefloor and others added 4 commits September 11, 2026 12:18
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StreamScreenShareDialog takes an optional controller, so the modal's
header and footer are reachable from a test and from a golden. The
golden used to hand-roll a copy of the chrome, which would have kept
passing while the real dialog changed underneath it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A controller starts reading the platform as soon as it exists, so
creating one lazily on first build made that a side effect of building.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.13187% with 25 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v2@ad336b1). Learn more about missing BASE report.

Files with missing lines Patch % Lines
.../lib/src/screen_share/desktop_screen_selector.dart 90.69% 8 Missing ⚠️
.../theme/components/screen_share_selector_theme.dart 53.33% 7 Missing ⚠️
...c/screen_share/screen_share_selector_defaults.dart 93.02% 3 Missing ⚠️
...zations/stream_video_flutter_localizations_nl.dart 0.00% 2 Missing ⚠️
...c/screen_share/screen_share_source_controller.dart 96.82% 2 Missing ⚠️
...dgets/design_system_candidates/stream_tab_bar.dart 95.23% 2 Missing ⚠️
...ideo_flutter/lib/src/theme/stream_video_theme.dart 85.71% 1 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff          @@
##             v2    #1342   +/-   ##
=====================================
  Coverage      ?   32.15%           
=====================================
  Files         ?      378           
  Lines         ?    28992           
  Branches      ?        0           
=====================================
  Hits          ?     9321           
  Misses        ?    19671           
  Partials      ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

macOS leaves the bitmaps out of the getDesktopSources result entirely —
the `thumbnail` key is commented out natively and thumbnailSize comes
back as {0,0}. They arrive only as events raised by the enumeration, and
the plugin's own getSources then rebuilds its source map from the
bitmap-less response, dropping whatever those events had delivered. The
old picker's two-second timer was the only thing that ever put an image
on screen, one tick after the dialog opened.

The controller now subscribes to onAdded and onThumbnailChanged before
the first load — getDesktopSources passes forceReload:YES, so every
source is re-added with its bitmap — and keeps the bytes by source id in
its own state, where a reload cannot clobber them. A capture pass is
asked for only if something is still missing afterwards, so the
platforms that report bitmaps inline pay nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant