-
Notifications
You must be signed in to change notification settings - Fork 1
Window manager
This document records design behaviour, including planned work. It is not a compatibility or feature acceptance report; see compatibility.
M8 replaces external scrcpy windows with app windows composited inside the DroidPier workspace. This is the behaviour the UI implements. Written before the widgets, because the hard parts here are ownership and state, not painting.
| Behaviour | Today | M8 needs | Gap |
|---|---|---|---|
| App windows | External, owned by scrcpy | Composited in the workspace | All of it |
| Dock/taskbar entries | Focus and close only | Focus, raise, minimise, restore, close | Extend |
| Focus |
isFocused flag, no visual window |
A focused frame that takes keyboard | New |
| Z-order | None | Explicit, independent of focus | New |
| Geometry | None | Move, resize, snap, maximise | New |
| Minimise/restore | None | Round-trip via dock | New |
| Input routing | scrcpy's own window handled it | Pointer and keys reach the focused app | New, contract-dependent |
| Empty workspace | "Open an app" desk | Same desk, unchanged | None |
| Disconnected / recovery | Overlay covers everything | Windows must survive and restore | Extend |
M8 requires two apps simultaneously visible, movable, resizable and independently minimisable. Tabs give one visible surface at a time, so they cannot satisfy it. Freeform it is — the cost is that we must implement drag, resize, snapping and z-order ourselves, which the rest of this document is.
The backend owns geometry, z-order, display state and focus. The UI sends intent and renders what comes back; it never stores a window's position.
This matters most during a drag. The tempting design keeps a local offset and reconciles later, which means the window is briefly showing something the backend has not agreed to — and if the backend rejects or clamps the move, the window jumps. Instead the UI streams intent during the gesture and renders only returned state. If that proves visibly laggy on real hardware, the fix is a backend that answers faster or an explicit interpolation contract, not a second source of truth in the UI.
Focus. Clicking anywhere in a window focuses it and raises it. Focus is
distinct from z-order — a window can be raised without focus (via the dock) and
focused without moving in z (it is already on top). The focused window takes a
signal-coloured frame; unfocused windows keep a hairline border.
Drag. By the title bar only. The body belongs to the Android app and its input. Dragging past the workspace edge clamps rather than allowing a window to be lost; the title bar always stays reachable.
Resize. Eight handles, 8 px hit slop outside the frame so a 1 px border is still grabbable. Minimum 240×180 — below that an Android app is unusable and the frame chrome dominates.
Snap. Drag to an edge to half-screen, to a corner for a quarter, to the top to maximise. Preview overlay before release, so a snap is never a surprise.
Minimise. The window leaves the workspace and stays in the dock with a dimmed running-dot. Restoring returns it to its previous geometry, which the backend must remember — the UI deliberately does not.
Title-bar menu. Right-click the title bar for snapping to either half or any quarter, Move to desk n for each desk the window is not already on, Maximise or Restore, Minimise, Fullscreen, Close, Close others, and turning the window Portrait or Landscape. The orientation entry names where it will go rather than where it is, and it inverts the content aspect rather than the frame's — swapping the frame's own width and height would leave the video letterboxed by exactly the title bar's height. Entries appear only where the window API can perform them. There is no always-on-top anywhere in that API, so it is not offered even in a disabled form. Move-to-desk is offered because the API moves windows between desks; it disappears where the host has no desks, such as a test harness, rather than appearing and doing nothing.
When no video arrives. A window can be streaming, hold a texture, and never have a frame painted into it. That renders as a black rectangle with a lit Live badge, which is indistinguishable from an app showing black, so the window says so instead and offers to reopen.
Saying it accurately is harder than it looks. A still app — a paused video, a page nobody scrolls — also presents zero frames per second and is working perfectly, and no amount of waiting separates the two, because a still screen stays still for as long as the person keeps reading. What separates them is whether the window has ever painted, so that is what is tracked. A rate that has not been reported yet is also not the same as a reported zero, and is not treated as one.
Remembered placement. An application reopens where it was last left, at the size it was, and maximised if it was. Placements are per package and are clamped into the current workspace, so a window arranged on a large monitor cannot restore beyond the edge of a smaller one. Minimised is not remembered — a window you come back to should not reopen hidden.
Maximise. Fills the workspace minus the menu bar and dock. Restore returns the pre-maximise geometry. Double-clicking the title bar toggles.
Close. Ends the session. The dock entry disappears. No confirmation: the Android app keeps running on the phone, so nothing is destroyed.
Keyboard. Keys reach the focused window's Android app. The shell keeps only Ctrl+Space (launcher) and Escape (dismiss overlay) — every other combination belongs to the app, or Android apps become unusable. Alt+Tab cycles focus and raises, Alt+Shift+Tab does the same backwards; the launcher and control centre remain reachable while a window has focus.
The switcher's order is captured when it opens and held until it closes. The shell rebuilds on every telemetry snapshot, which during streaming is continuous, so a list re-derived on each rebuild could reorder underneath the highlight — and releasing Alt would then focus a window the person never selected. A window closing mid-hold drops out of the list rather than leaving a stale entry behind.
Not just the streaming one. Each has been a source of dishonest UI elsewhere in this codebase, so each is specified:
| State | Frame shows |
|---|---|
starting |
Skeleton at the app's own colour, label "Opening…" |
streaming |
The surface |
suspended |
Last frame dimmed, "Paused" — never a blank box |
reconnecting |
Last frame dimmed with the rail's travelling trace |
failed |
Fault-bordered frame, the error's message, and a Retry |
closed |
Removed from the workspace entirely |
A window whose surface is null but whose status is streaming renders the
skeleton, not an empty rectangle. That combination means the backend has not
handed us pixels yet, and an empty rectangle would read as a broken app.
M8 names these; they are the acceptance bar, not a suggestion:
- two overlapping windows, both visible
- focus transfer between them, with z-order following
- minimise then restore, geometry preserved
- maximise then restore
- close
- launching from the app drawer into the workspace
- keyboard traversal reaching the focused window
Every golden image is opened and looked at before it is accepted. A green suite has already hidden a blank screen once in this project.
DroidPier source and downloads · Your Android. A bigger workspace.