DECORATIONS.md 5.9 KB

Server-side window decorations

The mjdev compositor (mjdevc) draws GNOME-style window frames itself, in the C shim (native/shim.c), instead of letting each app draw its own titlebar (CSD). The frame is rendered with cairo into an image surface that is wrapped in a custom wlr_buffer and shown as a wlr_scene_buffer node, so it is GPU-composited like any other surface and moves/raises/minimizes together with its window.

Policy (colors, what counts as an app window) is driven from the Kotlin side; the pixel-pushing lives in C. This follows the project's kotlin-first / compositor-only split: surface layering and frame rendering are genuine compositor concerns.

What the frame does

  • Look: rounded top corners, a soft vertical gradient derived from the desktop background color, a hairline top highlight, the window title (cairo "toy" font API — no pango dependency), and a 1px border down the sides and bottom.
  • Three GNOME-style buttons (right-aligned, order min · max · close):
    • minimize → hides the window (mjc_view_set_minimized)
    • maximize → toggles maximize (mjc_view_set_maximized)
    • close → closes the window (mjc_view_close)
    • hover highlights the button; the close button turns red on hover.
  • Move: drag the titlebar (anywhere that isn't a button) to move the window.
  • Resize: drag any edge or corner. An 8px hotzone around the frame starts an interactive resize with the correct edges; works on all four sides + four corners.
  • Cursors:
    • over the resize hotzone → directional resize cursor (n/s/e/w/ne/nw/se/sw-resize),
    • over a button → pointer (hand),
    • over the draggable titlebar → default arrow.
  • Active/inactive styling: the focused window's titlebar is brighter; unfocused windows are dimmed.
  • Focus-follows-mouse: moving the pointer over an app window focuses it (no restack) — MJC_FOCUS_FOLLOWS_MOUSE in shim.c.

Which windows get a frame

A window is decorated when it is a normal app window. It is not decorated when:

  • it is shell chrome — the desktop's own wallpaper, panels, menus, control center (the Kotlin Policy marks these via mjc_view_set_chrome(view, true)),
  • it is chromeless — either the shell asked for it (mjc_view_set_decorated(view, false) / IPC set-decorated), or the client itself demanded client-side decorations through the xdg-decoration protocol (we honor the client and leave it frameless),
  • it is an X11 override-redirect surface (menus, tooltips, popups).

Decoration is decoupled from the stacking layer, so always-on-top and always-on-bottom app windows keep their frame.

Theme / colors over IPC

The frame colors come from the shell's live background palette, sent over the IPC socket. Until the shell sends a palette, a dark-slate default is used.

The desktop shell pushes them automatically: ThemeManagerLinux.createFromPalette() (invoked whenever the wallpaper/palette changes) calls CompositorControl.setDecorationTheme(backgroundColor, textColor) (shared/.../helpers/compositor/CompositorControl.kt), a one-shot IPC sender that no-ops when not running inside mjdevc. So changing the wallpaper re-colorizes every window frame.

// rgb channels 0..255; fg is optional and defaults to a contrasting
// black/white chosen from the bg luminance
{"cmd":"set-decoration-theme","bg_r":30,"bg_g":34,"bg_b":42}
{"cmd":"set-decoration-theme","bg_r":30,"bg_g":34,"bg_b":42,"fg_r":240,"fg_g":240,"fg_b":240}

Other decoration-related IPC commands:

{"cmd":"set-decorated","id":3,"decorated":false}   // chromeless / frameless
{"cmd":"always-on-top","id":3,"on":true}
{"cmd":"always-on-bottom","id":3,"on":true}

C / native API (native/shim.h)

void mjc_view_set_chrome(mjc_view *view, bool chrome);       // shell surface, no frame
void mjc_view_set_decorated(mjc_view *view, bool decorated); // chromeless toggle
void mjc_set_decoration_theme(mjc_server *server,
    float bg_r, float bg_g, float bg_b,
    float fg_r, float fg_g, float fg_b);                     // colors in 0..1

Build / packaging

  • compositor.gradle.kts adds cairo to the shim's pkg-config cflags and links it into mjdevc.
  • libcairo2 is declared in gradle/libs.versions.toml (app-compositor-runtime-deps), the single source of truth that feeds the deb Depends, the installDesktop apt step and make-iso.sh. mjdevc (the binary that links cairo) ships via the deb and the ISO, so those are the package formats that need the dependency.

Geometry constants (shim.c)

#define MJC_DECO_TITLEBAR_H 36   // titlebar height
#define MJC_DECO_RADIUS     10   // top-corner radius
#define MJC_DECO_BORDER     3    // side/bottom border width
#define MJC_DECO_BTN_R      9    // button circle radius
#define MJC_DECO_BTN_GAP    8    // gap between buttons
#define MJC_DECO_PAD        14   // title-left / buttons-right padding
#define MJC_DECO_RESIZE     6    // edge/corner resize hotzone half-width

Next steps / known limitations

  • Maximized windows: the titlebar is drawn directly above the content. When a window is maximized to the full output, the titlebar sits at the very top (under the shell top bar). Inset/work-area-aware maximize is a follow-up.
  • GTK apps that ignore xdg-decoration still draw their own CSD titlebar, so they can show a double titlebar. A per-app override list can suppress this later.
  • Window transparency (ARGB) + blur ("glass"): the cairo buffer is already ARGB (alpha-capable), so the frame foundation supports translucency. True window transparency and background blur (frosted-glass) are a deliberate future step — they need a blur pass in the render path (e.g. a scenefx-style wlroots scene), not just the decoration layer.
  • Drop shadows: not yet drawn (kept off for now to avoid per-frame overdraw cost); belongs with the same blur/scenefx work above.