Build your own client.
Keep the engine reusable.
The library API, component composition and rendering lifecycle, independent of Lucent desktop preferences or Omarchy installation.
Build and VM status: this site's source links identify the documentation build. The VM can run a separate experimental release. Current readiness identifies accepted main, the deployed candidate, source-specific native results and remaining work.
The host service observer report exercises independent consumers of AudioPort, BluetoothPort, PowerPort and CompositorPort, supplied by the PulseAudio, BlueZ, PowerServices and Hyprland adapters. On source 6f9e75d0, four mock tests, strict release Clippy and four binary builds pass; each actual host consumer observes for 15 seconds, exits successfully and retires its owned unit. The audio watch returns after its own cancellation. No renderer, controls or input are needed for these observations. They establish read-only service access, not audible audio, keyboard events/pairing, physical controls, state-change delivery, hotplug, suspend, desktop UI or performance. Power's count-only format cannot distinguish absent and unavailable capabilities.
Four engine crates.
One optional design system.
The reusable framework consists of lucent-api, lucent-ui, lucent-render and lucent-wayland. Clients choose their own application state, components and service adapters. lucent-design is an optional set of tokens and recipes. None of the engine crates depends on the desktop client, its domain services or Omarchy.
If you want to change launcher size, wallpaper, keybindings or startup, use the desktop configuration guide. If you want to implement a new client, start here and run the small example:
cargo run --manifest-path lucent/Cargo.toml --locked -p hello-layerThis is an in-workspace library. The repository does not promise a stable published crates.io API. The example is a real independent client, not an Omarchy service.
Render a count.
Keep the value in the client.
Element::masked_input(secret.len(), "Password", Message::Edit)
.id("password")
.autofocus()
.on_submit(Message::Submit)The element stores a character count and placeholder. Layout paints bullets. Its callback receives InputEdit intents: Insert, Replace, Backspace or Clear. The framework never keeps the full secret in its input buffer or hit value; Debug output hides each edit's text.
The client owns validation and actual storage. Lucent's network form uses a zeroizing domain Secret and moves it into an owned command. Cancellation, stale-form generations and retries belong to the client. The control knows nothing about Wi-Fi.
on_submit works with ordinary and masked fields. Enter emits the supplied message, pointer input focuses, and Tab/Shift+Tab traverse controls and reveal scrolled fields. Transient event strings and external protocol copies are not guaranteed to be erased. Secure fields retain end editing; ordinary fields have the separate declarative text API below. Secure clients keep ordinary IME disabled. The merged text implementation adds explicit ordinary-client opt-in. Five owned native IME feature checks and five restoration checks pass; third-party IME acceptance remains pending, and platform accessibility is user-deferred.
Ordinary text has explicit selection state.
Element::text_input(&self.value, self.field.selection(), "Search", Message::Edit)
.id("search")
.autofocus()
// In update: use the current client value, not the painted snapshot.
let change = self.field.update(&mut self.value, edit, 8192,
ClipboardTarget::new("panel", "search"), effects, Message::Edit)?;
if change.content { /* refilter or request a save */ }
effects.redraw("panel");The framework shares pointer, caret and selection geometry, supports grapheme/word navigation and uses relative messages for input bursts before the next frame. The component retains the string; cursor movement alone does not save preferences. The text editing guide explains the complete state/input/rendering path and clipboard transfer and source-specific limits. The international-text guide describes the merged script/bidi/fallback path and ordinary IME adapter; Text 8e6d074b retains hard-paragraph levels across soft wraps through exact row-bound ResolvedBidiLine in clonable LayoutOptions/Paint::Text. It uses line-local upstream L1/L2, preserves tab separator semantics and discards stale context before cache lookup. All six CI checks pass at that source, including the full workspace, controlled service protocols and 832 Vulkan visual cases. The owned native IME suite also passes its five feature and five restoration checks; third-party, physical and cross-output text acceptance remains pending. The independent native example uses the same API.
Clipboard is opt-in through Application::clipboard_enabled. Ctrl+C/X/V queues an effect addressed to the live ordinary field; the native adapter negotiates UTF-8 and transfers through bounded nonblocking pipes on calloop. Completion returns to the owning child. Its ticket checks the current value and selection; native focus and surface lifetime are checked separately. WritePosted describes a queued selection request, not confirmed ownership. Wallpaper search and hidden-network names also use this control; wallpaper submission/provider changes retire old tickets, and network forms validate their generation and 32-byte name limit. Passwords remain separate count-only controls. Secure clients remain disabled. Read the guide for byte/deadline limits. Notes acceptance includes single-output compositor copy/cut/paste with wl-copy/wl-paste and restart persistence; The later real Chromium/Notes run passes nine keyboard-transfer checks in both directions and twelve restoration checks on unchanged 8e, one virtual output. Rich content, other apps, delayed transfers and focus/seat retirement remain separate checks.
In the merged library, Application::input_method_enabled() and Application::activation_enabled() are independent opt-ins, each defaulting to false. Text fields keep the canonical String in the component; transient preedit stays in the runtime presentation. Fresh activation uses Effects::activation_token(app_id, callback) only during a physical input dispatch. Its app ID is an Option<String>: use Some(app_id) for a known application or None when an injected UriPort resolves the default handler. The client maps completion to the domain value ApplicationLaunchContext before calling ApplicationPort or UriPort. Concrete implementations such as XdgApplications and XdgUri are adapters; the framework never imports XDG launch policy. A token or successful command start is not proof of browser focus. Login, lock and authorization keep both capabilities disabled. These library contracts are separate from desktop settings.
Notifications use the same component path.
The desktop's notification child declares cards and grouped history with Element, controlled scrolling and Motion. Its state owns grouping, paging and reveal timing. The injected notification adapter owns native delivery and takes a separate history adapter. One bounded storage worker performs file I/O while snapshots, signals and expiry continue; completion versions prevent stale loads/saves from changing newer state. Renderer and Wayland crates know no notification IDs. Closing retires the surface after a settled submission, and interrupted transitions preserve their current value. Read the implementation guide for action versions, archive recovery and the exact registration/update/render path.
The recorded 9eb trial, harness 04432a73 on runtime 9eb41939, adds natural session-owned history close and real keyboard delivery to the earlier R9 route-only evidence. Pointer and Tab/Enter routes each load one exact selected document with fresh context through the controlled temporary MIME default in an already warm real Chromium profile. Five route checks, nine cleanup checks and two trusted, unmodified, nonrepeating a / KeyA document receipts pass. The final pre-key and post-event guards both observe closed history, normal-toast input policy and advanced native submissions, without focus/hide intervention or key retry. The child owns current-request/history-session retirement; the domain keeps UriPort, its XdgUri adapter forwards context, and the framework supplies generic physical-input-bound activation and effects. PR105 accepts client fix 194c0c57; PR106/main bdffd424 now accepts the renderer pool and has identical Rust/design/configs/scripts trees to archived 9eb41939. The report records this later source equality separately, preserving its original pre-merge flags; no rebuild or native rerun is implied. Cold-browser focus, arbitrary handlers, direct Chromium token consumption, hardware and performance remain unverified. Older reports keep their original scopes.
State, messages and a view.
pub trait Component: 'static {
type Message: Clone + Send + 'static;
fn view(&self, cx: &ViewContext) -> Element<Self::Message>;
fn update(&mut self, message: Self::Message, effects: &mut Effects<Self::Message>);
}
/// An application supplies surfaces and subscriptions in addition to its component tree.
pub trait Application: Component {
/// Compositor input methods require explicit ordinary-client authority.
/// Authentication clients leave this disabled, independently of clipboard/IPC.
fn input_method_enabled(&self) -> bool {
false
}
/// Fresh native launch-intent tokens are opt-in; secure clients keep this off.
fn activation_enabled(&self) -> bool {
false
}
/// Clipboard protocol integration is opt-in. Secure clients keep it disabled.
fn clipboard_enabled(&self) -> bool {
false
}
/// Register font bytes once. Elements select a face by index; an empty list uses Fontconfig.
fn fonts(&self) -> Vec<&'static [u8]> {
vec![]
}
fn name(&self) -> &'static str;
fn surfaces(&self) -> Vec<SurfaceSpec>;
fn init(&mut self, _effects: &mut Effects<Self::Message>) {}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
vec![]
}
fn event(&self, _event: Event) -> Option<Self::Message> {
None
}
fn command(&self, _command: &str) -> Result<Option<Self::Message>, String> {
Err("Unknown command".into())
}
fn inspect(&self) -> String {
"{}".into()
}
/// Translate a scheduling failure into a client message. Unhandled failures
/// stop the runtime with an error (secure clients retain their lock protocol).
fn runtime_failure(&self, _failure: RuntimeFailure) -> Option<Self::Message> {
None
}
fn animating(&self, _surface: &str, _now: f64) -> bool {
false
}
fn animating_instance(&self, surface: &SurfaceAddress, now: f64) -> bool {
self.animating(surface.role, now)
}
}A Rust struct holds state. A message enum names possible changes. view(&self) constructs an Element tree; update(&mut self) changes state and records effects. An application also declares native surfaces, startup work and subscriptions. These are ordinary Rust method calls, not automatically discovered classes or registered DOM nodes.
| API value | Responsibility |
|---|---|
| Element<Message> | Rows, columns, stacks, grids, text, images, inputs, sliders, bounded vertical scroll views, styles and interaction messages. |
| ViewContext | Current logical role and optional concrete instance, logical width/height, exact rendering density and monotonic time. |
| SurfaceSpec | Role, optional instance and output target, layer, anchor, logical edge margins, size, reserved space, keyboard mode, visibility, pointer policy and optional redraw group. |
| Effects | Redraw requests, queued background tasks and exit requests. A redraw records intent; it does not submit GPU work. |
| Subscription | A stream or interval producer, keyed by an application-chosen ID, with cooperative cancellation. |
| Motion | CPU interpolation between values at a supplied time. Duration and easing are selected by the client. |
Compose views.
Delegate messages explicitly.
impl Component for Example {
type Message = Message;
fn view(&self, cx: &ViewContext) -> Element<Message> {
let theme = Theme::new(false);
theme.apply(
Element::column(vec![
Element::text("Hello, native Wayland").font(font::TITLE),
Element::text(format!(
"Output target: {} · {} available",
self.output
.as_ref()
.map_or("compositor choice", OutputName::as_str),
self.outputs.len()
))
.font(font::CAPTION)
.wrap(2),
self.counter.view(cx).map(Message::Counter),
Element::text(format!("Demo level: {:.0}%", self.level * 100.)).font(font::BODY),
theme
.slider(
SliderSpec::new(self.level, 0.05).expect("controlled normalized value"),
Message::Level,
)
.id("demo-level"),
Theme::new(false)
.button("Close", Message::Quit)
.id("close")
.background(Theme::new(false).surface_container),
])
.padding(space::XXL)
.gap(space::MD)
.size(example::WIDTH, example::HEIGHT)
.background(Theme::new(false).surface)
.radius(radius::PANEL),
)
}
fn update(&mut self, message: Message, effects: &mut Effects<Message>) {
match message {
Message::Counter(message) => effects.delegate(
|child| self.counter.update(message, child),
Message::Counter,
),
Message::Level(event) => {
self.level = event.value;
effects.redraw("example");
}
Message::Outputs(outputs) => {
self.outputs = outputs;
effects.redraw("example");
}
Message::Quit => effects.quit(),
}
}
}
impl Application for Example {
fn name(&self) -> &'static str {
"hello-layer"
}
fn surfaces(&self) -> Vec<SurfaceSpec> {
vec![SurfaceSpec {
id: "example",
instance: None,
output: self.output.clone(),
layer: Layer::Overlay,
anchor: Anchor::Center,
margins: Default::default(),
redraw_group: None,
width: example::WIDTH as u32,
height: example::HEIGHT as u32,
exclusive_zone: 0,
keyboard: Keyboard::OnDemand,
visible: true,
pointer_input: lucent_api::PointerInput::Content,
}]
}
fn event(&self, event: Event) -> Option<Message> {
if let Event::OutputsChanged(outputs) = event {
Some(Message::Outputs(outputs))
} else {
None
}
}
fn animating(&self, _: &str, now: f64) -> bool {
self.counter.motion.is_some_and(|m| m.active(now))
}
fn inspect(&self) -> String {
format!(
"{{\"level\":{},\"count\":{}}}",
self.level, self.counter.count
)
}
}Element::map wraps a child's messages in the parent's message type. Effects::delegate invokes the child's update and merges its effects, wrapping background results too. The runtime calls the application's update; it does not automatically locate and call an update method on every node.
Use stable element IDs where focus, hover or input state must survive rebuilding a view. Each visible native surface gets its own view. Child coordinates are resolved relative to their parent's content box. A surface anchor is a compositor request; a row's padding is a layout instruction inside that surface.
For a bounded card, Anchor::TopLeft requires explicit width and height; SurfaceMargins requests its position from the output edges. The runtime validates anchor/dimension combinations and unique IDs before native changes. The compositor decides actual placement. Output selection uses a separate optional SurfaceSpec.output with a validated OutputName. Missing named targets wait for registry availability; margins do not select a monitor. Fractional density has a separate optional protocol path. One-output live-video resize and integer/fractional-density retargeting pass in the native display-fit report; simultaneous outputs, secure clients and hotplug remain separate acceptance.
Event::OutputsChanged publishes typed catalog observations through the ordinary application event/message/update path. The hello-layer --output DP-1 example declares a target and consumes that catalog. The native backend resolves a current output object before creating its layer role and retires/recreates the role when its target changes. Read the output guide for registry registration, waiting, closure, input lifetime, secure coverage and deferred native placement/hotplug acceptance.
One role can have multiple concrete surfaces.
cargo run --manifest-path lucent/Cargo.toml --locked -p multi-outputSurfaceInstance distinguishes copies of one logical role. The runtime keys native state by typed SurfaceAddress, while ViewContext.instance selects client-owned state. Capture that value in messages or use Element::map; two bars can reuse the same element IDs with separate focus and pointer capture. Event::Scoped preserves the concrete instance around native input/lifecycle events. Global output catalogs remain global.
fn surfaces(&self) -> Vec<SurfaceSpec> {
self.bindings()
.into_iter()
.map(|(instance, name)| SurfaceSpec {
id: "bar",
instance: Some(instance),
output: Some(name),
layer: Layer::Top,
anchor: Anchor::Top,
margins: Default::default(),
redraw_group: None,
width: 0,
height: (component::input::HEIGHT * 3.) as u32,
exclusive_zone: 0,
keyboard: Keyboard::OnDemand,
visible: true,
pointer_input: PointerInput::Content,
})
.collect()
}Effects::redraw_surface(address) targets one instance; role/group redraws still reach every matching instance. Application::animating_instance reports per-instance frame demand, and ClipboardTarget::in_instance scopes field transfers to that exact native address. Read the instance guide for registration, routing, retirement and the native checks still deferred. The sample owns connected transient state. The separate desktop monitor implementation composes this API over persisted domain profiles. Fractional density is implemented, and one-output live-video density migration has native evidence. The later desktop-based 16-check native run passes two simultaneous outputs, one owned headless, with real panel input at 1×/2×/1.25× and lifetime retirement/recreation. It does not repeat this independent example’s clipboard workflow or establish physical/cable/secure-output, suspend or presentation acceptance.
A later desktop-client nine-flow Display run, harness 1774fa87 on runtime 26d03adf, covers draft/Apply/Revert/timeout/Keep, rotation, disable/re-enable and native pointer/keyboard input on the fractional peer panel. The panel uses 1024×768 logical units, density 1.25 and a 1280×960 buffer. Five positive preservation checks, one ctime/link-count scope marker, three cursor checks and independent later normal-OFF verification pass, including the same desktop lifetime and unchanged 14 source/43 security entries. Display transactions remain desktop domain/use-case/adapter policy; the library supplies scoped surfaces, density, buffers and input routing. This does not repeat the independent example’s clipboard test or establish physical monitors/cables, suspend/resume, compositor restart, secure-output changes, resource targets or presentation FPS.
The completed FHD video measurements on archived 9eb41939 use one virtual 1920×1080/60 Hz/1× output and the same 4K H.264 60 FPS source. With tracing OFF, 480 samples over 120.000454 seconds record 497.063 MiB mean/peak RSS, 616.008 MiB process-lifetime HWM, 142.633% of one logical core and zero process swap. A separate 120.000898-second instrumented process observes 7,179 distinct prepared-revision advances over 119.983329 compositor seconds: 59.833/s, with 16.667 ms p50/p95/p99 and 50 ms maximum feedback gaps. Both phases are host-pressure-ineligible. Their different process births prevent a memory-gain comparison; bound-resource feedback establishes neither visible pixels nor physical scanout or accepted 60 FPS. Preservation, driver restoration and a separate terminal-guardian/normal-OFF check pass. PR108/main fb7b1b26 adds CPU frame reuse, but this tested VM predates it. Memory investigation, eligible targets and broader hardware/application tests remain open.
The verified delivery checkpoint is PR108/main fb7b1b26, Pages run 40044620948: three HTML pages, native manifest/PNG and 116 inputs/480 captures from b3c91fab match the artifact; the same bundle passes 20 LAN HTTP checks. CI builds and publishes later main revisions through their own receipts. The earlier 9eb trial, 4K setup/restoration follow-up and failed V1 coverage attempt retain their recorded outcomes. The completed V2 phases do not establish the actual V1 failure cause or a PR108 gain.
The historical 26d03adf 4K A/B output preflights verify a 2560×1440 logical background at density 1.5 with a 3840×2160 buffer and producer frame, then restore the original output. Both resource samplers refuse admission, so no resource interval was collected. A separate one-off diagnostic after restoration at FHD records 604.594 MiB process RSS, 539.586 MiB anonymous RSS, 16.242 MiB process swap and 680.293 MiB process-lifetime high-water RSS. These observations motivate memory investigation; they neither identify an allocator/decoder/renderer or GPU cause nor establish a leak, eligible budget, 4K causal effect or presentation rate. Framework density and geometry checks remain separate from desktop resource acceptance.
Effects::recreate_surface(address) renews an ordinary non-reserving layer-shell surface's native lifetime without changing its logical address or monitor routing. The runtime disables the old surface's pointer/keyboard input and retains one last-good presentation until its replacement successfully submits a frame. Repeated requests keep that successful presentation rather than an unpainted pending replacement. Native input, retry and frame events from retired objects cannot target the new lifetime. Logical frame counts and bounded timing/profile history continue; native_frames counts the current renderer separately. Hiding/removing the address, failure or shutdown releases both lifetimes. Positive committed, pending or replacement exclusive zones are rejected before native mutation; reservation transfer between surfaces has no atomic protocol guarantee. Secure session-lock clients reject recreation before executing the effect batch. This handoff adds no worker or polling; native compositor acceptance remains separate from the policy tests.
Density is a validated positive ratio, carried by ViewContext and Resize separately from integer output metadata. The native adapter uses fractional-scale plus viewporter when both are available. Layout and input remain logical; the GPU receives checked physical extents and actual per-axis clip sampling ratios. Text rasterization uses that density while preserving logical advances. Read the density guide for registration, fallback, ownership, frame submission and its original acceptance scope. Later single-output live-video checks cover integer/fractional retargeting, without establishing simultaneous-output, secure-client or hardware behavior.
PointerInput::Content uses the tree's hit coverage; All covers the whole native extent; None lets pointer events pass through even when a decorative preview contains interactive elements. Keyboard policy is separate. An optional redraw_group lets one Effects::redraw invalidate related buffers; exact IDs still work. No extra redraw is scheduled while idle.
Element::input_only() retains a subtree's layout, focus and pointer capture while suppressing all its paint. Lucent uses this for stationary drag sources beneath a full-display preview. This differs from zero opacity, which removes the subtree from interaction. Widget grouping, spacing and drag policy stay in the desktop client; the framework knows only surfaces and trees.
The example's slider is controlled by the client: SliderSpec::new(value, step) validates normalized values, and each SliderEvent returns the new value and whether the gesture finished. Its update stores that value and requests a redraw. Pointer capture, arrow keys, Home/End and focus belong to the framework; units and OS effects belong to the client. Theme::slider supplies semantic colors and spacing tokens. This demo does not control system audio. The desktop audio panel composes the same slider and scroll APIs over its injected AudioPort adapter; its native validation is tracked separately.
A scrolling list uses the same path.
cargo run --manifest-path lucent/Cargo.toml --locked -p scroll-listimpl Component for Example {
type Message = Message;
fn view(&self, _: &ViewContext) -> Element<Message> {
let theme = Theme::new(false);
let content = Element::column(
(0..self.count())
.map(|index| {
theme
.button(format!("Item {:02}", index + 1), Message::Select(index))
.width(Length::Fill)
.height(Length::Fixed(token::ROW_HEIGHT))
.background(if self.selected == Some(index) {
theme.primary
} else {
theme.surface
})
.color(if self.selected == Some(index) {
theme.on_primary
} else {
theme.on_surface
})
.id(format!("item-{index}"))
})
.collect(),
)
.width(Length::Fill)
.gap(space::SM);
let viewport = if self.hidden {
Element::empty().fill()
} else {
theme
.scroll(
ScrollSpec::new(self.offset, scroll::LINE_STEP).expect("controlled offset"),
content,
Message::Scroll,
)
.id("items")
};
theme.apply(
Element::column(vec![
Element::text("A list, owned by its client").font(font::TITLE),
Element::text("Wheel · drag the thumb · Tab · Page Down").font(font::LABEL),
viewport,
Element::row(vec![
theme.button("Resize", Message::Compact).id("resize"),
theme.button("Fewer", Message::Short).id("fewer"),
theme.button("Hide", Message::Toggle).id("hide"),
theme.button("Close", Message::Quit).id("close"),
])
.gap(space::SM),
])
.fill()
.padding(space::XL)
.gap(space::MD)
.background(theme.surface)
.radius(radius::PANEL),
)
}
fn update(&mut self, message: Message, effects: &mut Effects<Message>) {
match message {
Message::Scroll(event) => self.offset = event.offset,
Message::Select(index) => self.selected = Some(index),
Message::Compact => self.compact = !self.compact,
Message::Short => {
self.short = !self.short;
self.offset = 0.;
self.selected = None;
}
Message::Toggle => self.hidden = !self.hidden,
Message::Quit => effects.quit(),
}
effects.redraw("list");
}
}The client stores an offset and passes ScrollSpec::new(offset, line_step) with one content tree to Element::scroll. Both values use logical pixels. Give the viewport a fixed height or the remaining height of a bounded column; give the content intrinsic or fixed height. Layout measures the content, reserves the scrollbar gutter, clamps the presented offset and places content at viewport.y − offset. The renderer receives ordinary clipped shapes and text; it has no scrolling policy.
Wheel input starts at the topmost interactive hit and follows its actual scroll ancestors. It can reach an outer viewport at an inner limit, but it cannot scroll an unrelated list behind an overlay. A thumb drag captures the pointer until release, including outside the viewport. Removing the control or changing its measured range, track or clip cancels capture. Rebuilding with the same stable ID and geometry uses the latest callback.
Tab and Shift+Tab move through reachable controls and reveal off-screen descendants. Nested reveal can return multiple messages, applied before one redraw pass. The focused viewport handles arrows and Home/End; Page Up/Down also work from its descendants. Ordinary clipped content and rounded corners with no native input coverage are skipped. Application-level key handling runs first; return no message for keys the framework should handle.
Every scroll event returns a clamped offset. Store it and request a redraw; Element::map supports child message types. Layout does not mutate your saved offset when items disappear. This example resets its offset when replacing the data set; another client can preserve it and accept clamping. Scroll position has no persistence or OS effect unless the client supplies one.
Theme::scroll supplies semantic colors, padding, corner radius and generated component.scroll tokens. It remains optional. Views share immutable ancestor metadata, and keyboard traversal stops at the next reachable candidate. All list items are still measured and placed: this is bounded viewport scrolling, not a virtualized list. Touch, kinetic scrolling, horizontal scrolling and arbitrary programmatic focus/reveal remain separate work.
Formatted text stays reusable.
Element::rich_text composes TextSpan values with optional registered font faces, color, underline and opaque link identities. Word wrapping continues across style boundaries; long words break at graphemes. Each line shares a baseline and becomes ordinary text/shape paint. Wrapped links share one keyboard focus target while their individual fragments define pointer and native input coverage. The engine never interprets a link as a URL.
cargo run --manifest-path lucent/Cargo.toml --locked -p rich-textimpl Component for Example {
type Message = Message;
fn view(&self, _: &ViewContext) -> Element<Message> {
let theme = Theme::new(false);
theme.apply(Element::column(vec![
Element::text("Inline rich text").font(font::TITLE).color(theme.on_surface),
Element::rich_text(vec![TextSpan::new("One reusable flow supports "),TextSpan::new("bold").face(1),TextSpan::new(", "),TextSpan::new("italic").face(2),TextSpan::new(" and "),TextSpan::new("both").face(3),TextSpan::new(" faces.\nA "),TextSpan::new("long link wraps naturally across several lines while remaining one keyboard focus target").link("details").color(theme.info).underline(true),TextSpan::new(". More ordinary text follows.")])
.on_link(|id|Message::Selected(id.into())).id("body").autofocus().font(font::BODY).color(theme.on_surface).width(Length::Fill).wrap(20),
Element::text(if self.selected.is_empty(){"Tab to the link · Enter selects · Click any wrapped fragment".into()}else{format!("Selected client action: {}",self.selected)}).font(font::CAPTION).color(theme.on_surface).width(Length::Fill).wrap(2),
theme.button("Close",Message::Quit).id("close"),
]).padding(space::XXL).gap(space::MD).size(component::example::WIDTH,component::example::HEIGHT).background(theme.surface).color(theme.on_surface).radius(radius::PANEL))
}
fn update(&mut self, message: Message, effects: &mut Effects<Message>) {
match message {
Message::Selected(id) => {
self.selected = id;
effects.redraw("rich-text");
}
Message::Quit => effects.quit(),
}
}
}The notification client maps pure parsed content onto this control and uses its injected URI adapter for explicit link actions. The renderer preserves italic ink bearings while keeping layout advances stable. Read the rich-text guide for layout, clips, focus, native input regions, fonts and current shaping/IME limits.
The document belongs to the component.
Element::text_area takes a value, selection, placeholder, controlled ScrollSpec and edit/scroll callbacks. Use TextFieldState::multiline() to retain hard lines through the same bounded ordinary history and clipboard model. A shared source-indexed row map drives wrapping, pointer selection, vertical movement and visible-row paint; the engine retains only transient visual affinity and the horizontal navigation goal.
The native runtime refreshes CPU input geometry independently of GPU frame admission. Queued edits and row movement can therefore use current client state while a previous buffer awaits a compositor callback. Textarea painting emits visible rows individually, preserving raster density without creating one tall document texture. The current merged 8e6d074b text path shares script/bidi/fallback shaping and source/caret geometry, with ordinary-client IME opt-in. Its owned native IME suite passes five feature and five restoration checks; third-party/physical/cross-output IME and broader pixel/scroll details remain separate acceptance.
cargo run --manifest-path lucent/Cargo.toml --locked -p text-areaRead the multiline implementation guide for model ownership, wrap affinity, capture, clipboard lifetime, runtime flow and 17 passing native Notes functional checks and nine restoration checks. That historical clipboard run used wl-copy/wl-paste. Later real Chromium textarea ↔ Notes keyboard transfers pass all nine interaction and twelve restoration checks on unchanged 8e with harness 2044e54f, one virtual output. Native pixel/scroll details, other apps, rich content and delayed-transfer/focus races remain separate acceptance.
Images arrive through effects.
The notification client requests current-version image content through its presentation AssetPort. Its adapter reads local files or converts validated raw pixels on the existing asset worker, prepares immutable ImageData with mip levels and returns typed results. One active batch and latest pending demand bound the queue. Views reference prepared data through Element::image; the framework knows no notification source path or hint policy.
A newly prepared picture receives a client Motion using design tokens. Its opacity is sampled at ViewContext::now, and active motion reaches the runtime through Application::animating. Compositor callbacks pace dirty frames until it settles; Vulkan blends the supplied value. Bounded scroll viewports keep image-card actions reachable. Read the image implementation guide for domain values, adapters, preparation, clips, input, retention and measurements.
Anchor a label.
Own an explicit popup.
theme.button("Choose", Message::Open)
.id("choose")
.tooltip(theme.tooltip("Choose a value", OverlaySide::Above))The ordinary tree lays out first. A deferred overlay pass resolves the element ID against that surface’s actual bounds and clips. AnchoredSpec describes the preferred side, alignment, gap and viewport inset. Placement can flip and clamp; it cannot leave the native buffer or invent output-global coordinates. Missing, duplicate, clipped or invalid anchors cannot host the overlay.
A tooltip adds paint alone. Its content cannot add focus, native pointer coverage or input handlers. Pointer dwell uses one cancellable calloop deadline across live surfaces; waiting requests no animation frames or worker. The short fade uses ordinary compositor-paced frame callbacks, then stops. Keyboard focus shows the label immediately. Escape, source retirement and pointer leave hide it. Client tokens supply width, spacing, dwell and transition policy.
root.popover(Popover {
id: "choices".into(),
anchor: "choose".into(),
placement: AnchoredSpec {
side: OverlaySide::Below, align: OverlayAlign::Start,
gap: component::popover::GAP, inset: component::popover::INSET,
},
content: Box::new(theme.popover(menu_content)),
dismiss: Message::Close,
})One interactive popover per surface owns a focus scope. The runtime refreshes its input geometry before native input even when a GPU frame is pending. Control keys reach that scope before application-global keys: Tab stays inside, Escape dismisses, and an outside press cannot release a control underneath. Closing restores the still-live opener; losing the anchor cancels capture and supplies a dismissal. Stable unique element IDs and client lifetime validation remain required. This initial API creates no native xdg_popup, secure-session role or nested menu system.
cargo run --manifest-path lucent/Cargo.toml --locked -p anchored-overlaysThe independent client owns its opening Motion and selected value. Tray descriptions and dock names use the same tooltip API in the desktop. The implementation guide follows placement, lifecycle and the deliberate one-scope limit. Strict checks pass 717 regular Rust tests and 103 Python checks; all 712 software-Vulkan comparisons pass with 24 reviewed new references. Production bar/dock pointer checks and four static native captures also pass on one 1× output, with exact caller-state preservation. The scoped acceptance report retains those results and limits. The later independent component lab at b9de7ba4 passes native popover opening focus, Escape dismissal and opener-focus restoration on one prepared output. Native outside-click dismissal, keyboard focus trapping, popover clipboard interoperability, bar click-through, precise fade timing, smoothness and current RSS still need their own evidence.
One event loop owns state.
The executable selects the runtime entry point. run enables the local command and inspection server; run_without_ipc runs ordinary Wayland surfaces with that server disabled. Permission dialogs use this explicit mode, and run_greeter delegates to it for login. Secure lock clients use run_locked, which protects every output through the session-lock protocol and requires the client's authenticated result before unlocking.
Returning from a runtime invocation also tears down its native surfaces. The runtime drops each GPU surface before its Wayland role, then flushes queued destruction requests while the connection is alive. This matters for a permission agent that remains running after an individual dialog closes. The same cleanup runs on error returns; destroying presentation resources never authorizes a session unlock.
run(application)
connect to Wayland; create shared GPU and layout resources
register event sources with calloop
call init; reconcile surfaces and subscriptions
wait for events; dispatch the available batch
inside dispatch: message → update → effects → invalidate surface
after dispatch, once per drawable dirty surface:
configured + dirty + no pending frame
→ view → layout → paint and hit regions
→ wgpu/Vulkan commands → present buffer
Wayland frame callback → allow the next dirty framePointer and keyboard events arrive through Wayland. An optional local command socket translates external strings into application messages. Effects run on named ordered queues; independent queues and subscription workers can run concurrently. All return messages to the event loop, which owns the application state.
GPU painting happens once after the available event batch, using the resulting state. Input layout remains synchronous, so another click or key uses current controls while a compositor callback is pending. A successful submission can request one coalesced wake for newly drawable followup work; idle or callback-blocked surfaces do not poll. Read the event-batching guide for input capture, submission handoffs and frame admission.
Effects::task_on_with_failure(queue, run, on_failure) attaches a small message constructor to the specific operation. Rejection or panic returns its own typed result even when several features share a queue; Effects::delegate lifts both success and failure. The mapper must not block because rejected work is handled directly on the UI thread. Cancellation skips it. Without a mapper, or if the mapper panics, Application::runtime_failure handles the safe failure. The desktop declares eight lanes in work.rs; pairing answers share the short controls lane while native methods use their own lane.
Background delivery holds at most 64 waiting messages. A full queue pauses its producer; it never blocks the UI thread to enqueue. Subscription cancellation releases that wait and invalidates already queued events from the old lifetime. The runtime admits at most 64 live subscription bodies, including streams still stopping. Do not hold a lock needed by the UI while emitting. Payload sizes and service reconnection remain application/adapter responsibilities.
Subscription::map(ParentMessage::Child) lifts an injected child's stream into its parent without creating another worker. Its identity, cancellation and bounded emitter remain the same lifetime. For an operation that must cancel and report admission failure, use Effects::task_cancellable_on_with_failure. Lucent's media import uses this contract so closing retires pending delivery and queue rejection clears the specific import ticket.
The renderer owns shaders, primitive buffers, image/text caches and GPU submission. Hyprland combines our buffers with other windows. The compositor does not receive the Element tree. Frame callbacks pace further work; an idle surface does not redraw continuously.
ImageData::from_rgba prepares premultiplied color pixels. For monochrome artwork, ImageData::from_alpha prepares one coverage byte per pixel; the element's tint supplies its color. Both constructors prepare the same mip dimensions and area-filter rounding on an asset worker. format() identifies the storage, and ImageLevel.pixels contains tightly packed bytes in that format. Its type is now Arc<Vec<u8>>: migrate struct literals with pixels: pixels.into(). Existing vector constructors remain. The framework guide explains shared straight-RGBA preparation, alpha validation and copying rules.
Changing pixels use ImageStream. Construct one stream for a producer, then call frame_rgba(width, height, bytes) on that worker. Each returned ImageData owns immutable premultiplied bytes and one image level. Its allocation identity stays distinct from the stream's GPU resource identity and revision. The renderer uploads a changed revision into the existing texture when dimensions match; different dimensions replace it. A repeated revision needs no upload. Nothing in this API discovers files, decodes video, selects an output or schedules a wallpaper.
ImageData::lifetime() returns an ImageLifetime: a weak observation of the prepared image levels, without retaining pixels. Value clones and frozen snapshots keep it alive; a copied, smaller mip chain has its own lifetime. A producer retaining shared pixel storage does not keep these prepared levels alive. The renderer uses this observation to discard an unused image texture when its source has expired, even below the cache's reuse budget. Every canvas still pins its current draw resources, so another output cannot evict them. Stream updates replace the observation with the latest frame's lifetime; reusable text resources keep their normal cache policy. Cleanup happens during existing draws and canvas teardown, with no extra idle polling. Texture payload retirement does not guarantee an immediate decrease in process RSS or driver allocations.
The desktop's native media guide follows a real consumer from saved domain preferences through an injected playback adapter, a latest-frame mailbox, exact background invalidation, Vulkan upload and Wayland submission. This demonstrates why a producer event and a compositor frame callback are separate: the former changes content; the latter permits presentation of dirty work. A settled panel's submission event requests no further paint. Automatic battery/fullscreen pauses are pure domain/use-case decisions. The client joins port observations, retains a static image snapshot for a paused output, and keeps a shared producer only while another output needs it; the framework renderer has no wallpaper policy.
The Vulkan adapter uploads coverage as R8Unorm and reconstructs premultiplied white before applying tint and opacity. Text also uses a single coverage channel, keeping its existing 2× supersampling and output density. This reduces each mask's CPU and texture payload by 75%; it does not reduce its resolution. Color icons and wallpapers remain RGBA. Payload savings do not establish a process RSS improvement or total GPU-memory budget; native measurements cover those separately.
The shared GPU device requests MemoryHints::MemoryUsage to favor memory use over allocation throughput. This supported backend preference suits a UI with small scenes and long idle periods. It changes allocation strategy, not surface size or image density. A backend may ignore the hint; it does not cap active resource sizes, driver allocations or process RSS. Hardware frame pacing needs separate measurements.
A successful submission emits Event::FrameSubmitted after the surface's scene and input bookkeeping. It carries that buffer's logical width/height and the monotonic time used by its view. The application can turn it into a message; returning no message requests no extra redraw. This event confirms our submission, not GPU completion or display scanout. A Wayland frame callback separately permits the next dirty frame.
The desktop uses that event to hand the shared bar between native panel hosts. Client state tracks each output's previous and requested carrier, keeps outgoing controls inert and preserves the 48-pixel reservation. The framework still receives ordinary component trees and surface declarations. This client presentation policy does not make separate Wayland commits atomic or acknowledge scanout.
Keep display layout separate from native buffer size. ViewContext and input hits use surface-local coordinates. The desktop's settled dock requests a short bottom-anchored buffer, translates its existing display-grid position into that buffer, expands before opening and shrinks after a settled closing submission. Those choices belong to the client. Shadow::blur_outset exposes the renderer's finite shadow support so clients can retain all painted pixels when calculating a crop; it does not select a desktop's spacing tokens.
Logical dimensions become physical texture dimensions through checked multiplication by the current integer scale. Native surfaces and Canvas::snapshot reject dimensions beyond the requested GPU device's texture limit; snapshots also check aligned readback size and reserve their packed result fallibly. Device limits describe valid requests, not available memory. Fractional scaling remains separate work.
Event::Resize carries logical width/height, integer fallback scale and the actual validated Density. It arrives after surface configuration and when a configured surface's density changes. Clients use the density's checked physical-size conversion for display assets; layout and input remain logical. ImageData::downsample_for_cover copies only the coarsest prepared mip chain whose two dimensions cover that physical target. It preserves the original aspect through aspect_size(), even when odd mip sizes round down. Run this preparation on an asset worker. The framework neither reads a wallpaper file nor decides when a desktop selection commits.
Canvas::paint returns a result. The renderer validates scene geometry, image dimensions, text requests and clip-buffer capacity, then prepares resources before writing geometry or submitting a frame. A rejected scene retains the canvas's previous resources and target pixels; temporary uploads become eligible for cache eviction. This library behavior does not add automatic native recovery: the runtime currently treats a rendering error as fatal and follows its existing supervision policy.
Renderer admission limits are backend constraints, separate from design tokens: coordinates and numeric effects use a finite 2²⁴ envelope, text primitives accept at most 64 KiB of UTF-8, and supersampled glyph bitmaps are bounded before rasterization. Geometry buffers use the requested device's limits, including the storage-binding limit for clips. These checks neither cap total process memory nor validate every upstream layout operation or font parser.
For CPU-stage evidence, start the runtime with LUCENT_PROFILE_FRAMES=1. Inspection keeps the latest 120 successful frame samples per surface: view, layout, native input, buffer acquisition, preparation, encoding, submission and presentation calls. Text rasterization and uploads are nested within preparation. These are CPU wall times including driver waits; they are not GPU timestamps or display latency. Profiling is off by default and requests no extra frames.
The same opt-in also exposes a separate, bounded frame_pacing trace: existing callback requests and handler entry, raw callback timestamps, and every admitted draw attempt with a fixed outcome. Native retirement discards pending timing authority; overlapping requests make correlation ambiguous rather than joining a callback to the newest request. The timing guide covers the 512-event limit, missing/retired counters and unchanged successful-frame schema. This is private backend instrumentation: domain ports, client state and rendering policy are unchanged, with no added redraw or timer.
A separate native diagnostic at 91b964ae passes four execution and five restoration checks. Its mean request-to-handler interval is 44.175852 ms, handler-to-next-attempt 0.006112 ms and attempt duration 1.603372 ms; compositor feedback separately records 21.972535 intervals/s. Request-to-handler includes transport, scheduling and dispatch, so it is not compositor-only time. Process-relative nanoseconds, raw core callback milliseconds and presentation clock IDs remain separate. This main-based integration copies the diagnostic and timing code without the unaccepted renderer/video ancestors; its source and CI/delivery identities are separate from the 91b native run, and those checks do not extend that native scope. No causal or performance-target acceptance follows.
Read the detailed library implementation for layout, clipping, input, textures and scheduling. Follow the wiring traces this same engine through the actual desktop client.
Shared input recipes in lucent-design give ordinary fields a 48-pixel height, 16-pixel horizontal inset and 8-pixel vertical inset. Use Theme::text_input for a complete field, or input_group with text_input_content for leading icons and trailing buttons. The layout engine calculates caret positions from the same font metrics as the text. Placeholder dimming comes from the design token component.input.placeholder_opacity; the reusable SDK’s explicit style defaults to full opacity. Authentication uses count-only masked inputs for secrets; the client retains zeroizing storage.
Click interception and hover feedback are separate. A panel can consume outside-click routing while setting hover_feedback(false); its child buttons still highlight. Wallpaper crossfades belong to the desktop presentation component. It starts SDK Motion after the first frame for a new selection submits, so that frame's wait and rendering time do not consume the fade. Image opacity follows the timeline; at most one outgoing frame stays retained per output until a fully revealed view submits. Submission does not acknowledge compositor scanout. ImageData::snapshot freezes a streamed frame by sharing immutable pixels under a separate GPU identity. Vulkan draws each supplied opacity; the client timeline interpolates it.
Pointer actions use on_click, on_secondary_click, on_middle_click and on_wheel. The SDK owns topmost clipped hit testing and matching-button capture. Moving away, leaving the surface, removing the handler or changing its geometry retires alternate-button capture. WheelEvent carries horizontal and vertical logical lines, including fractions. A leaf handler consumes the event before ancestor scrolling. Element::map lifts these callbacks like any other child message; Effects::delegate keeps asynchronous work in the child. For the tray, the path is Wayland input → SDK interaction → Tray child message → injected TrayPort → StatusNotifier adapter → application D-Bus method. The engine knows nothing about tray applications or menus.
Keep product policy in clients.
Launcher sizing, widget snapping and Omarchy integration belong to the desktop. Generic layout constraints belong to lucent-ui; new surface protocol support belongs to lucent-wayland; texture sampling belongs to lucent-render. A service contract is a port. Its implementation is an adapter. Presentation data such as decoded image pixels belongs outside the pure desktop domain.
cargo fmt --manifest-path lucent/Cargo.toml --all --check
cargo clippy --manifest-path lucent/Cargo.toml --locked --workspace --all-targets -- -D warnings
cargo test --manifest-path lucent/Cargo.toml --locked --workspace
python3 -m unittest discover -s tests -vUse the repository's lucent-framework skill for library or architecture work. Scoped owned native tests pass IME composition/navigation and two-output density/input/lifetime handling. Real Chromium/Notes keyboard clipboard transfer now passes in both directions on one virtual output, with nine interaction and twelve restoration checks. Current limits include third-party IMEs, other clipboard apps/rich content/transfer races, physical/secure output changes and eligible performance measurements. Accessibility integration is user-deferred. One-output live-video density retargeting and Notes editing/clipboard/persistence retain their own scoped native evidence. Read the readiness guide before treating historical pending entries as the current summary. The architecture review separates checked boundaries from remaining design work.
A service child uses the existing SDK.
Weather is desktop code built with the library. The executable constructs an OpenMeteo adapter, injects it through the domain WeatherPort, and creates the Weather child. The root explicitly wraps its subscriptions and messages; the engine does not discover or register service objects automatically.
The child composes ordinary TextFieldState/TextEdit inputs, controlled scrolling, generated design recipes and Motion. Editing changes its draft. Apply returns a preference intent to the root's persistence coordinator. Closing retires form and clipboard authority. Layout, input, Wayland and Vulkan contain no forecast policy.
A generation identifies the existing subscription slot. Its producer sends Started/Observed, calls the adapter away from the UI thread, then sleeps between refreshes. A stable ID retains that producer. Idle location changes or explicit refreshes start a new generation; while a forecast request is busy, they coalesce into one latest demand until the bounded request completes. Old-location presentation clears immediately, and replies for coordinates no longer desired are ignored. Local behavior, reviewed visuals and scoped native opening/focus checks pass. One configured production forecast and a current resource sample are recorded; form changes, failure/retry, resource targets and pacing remain open. Follow the startup-to-frame guide and scoped evidence.