Migrating to v0.0.10¶
A canvas draws a pixel buffer as it was at the draw call¶
This affects scripts and apps that draw a pixel buffer onto a <canvas> and
then write to the buffer before the frame is drawn.
canvas::draw_buffer and canvas::draw_buffer_scaled now take the buffer's
pixels at the call, as drawImage does on the web. Before, a draw read the
buffer when the tick replayed the drawing, so a write later in the same
handler or tick showed up in a draw made before it. Now it shows only once the
buffer is drawn again, on every host, in a Rust app, and in a page.
A script that drew first and wrote after, expecting the write on screen, moves the draw after the write:
// before: the fill reached the screen through the draw above it
canvas::draw_buffer("art", buf, 0.0, 0.0);
canvas::buffer_fill_rect(buf, 0, 0, 8, 8, 0xff0000ff);
// after
canvas::buffer_fill_rect(buf, 0, 0, 8, 8, 0xff0000ff);
canvas::draw_buffer("art", buf, 0.0, 0.0);
A draw of a buffer handle that names no buffer now records nothing at the call, rather than being skipped when the drawing is replayed; what appears on screen is the same.
A recorded canvas fill carries its fill rule¶
This affects Rust code that builds or matches the canvas module's recorded
calls, lumen_canvas::ops::Op, directly. Scripts, and Rust apps that draw
through Context2d, are not affected.
Op::Fill is now Op::Fill(FillRule), because a fill can be nonzero or
even-odd (Context2d::fill_evenodd, canvas::fill_evenodd). Write
Op::Fill(FillRule::NonZero) where Op::Fill was, and match Op::Fill(_).
FillRule is exported from lumen_canvas and lumenui_canvas.
ShowContextMenu is now ContextMenuEvent¶
This affects Rust code that reads lumen_core::input::ShowContextMenu. The
message used to come only from an assistive technology's request. It is now
the one context-menu request every source raises: a right click, the
ContextMenu key, Shift+F10, an assistive technology, and a page's browser. It
carries where a menu belongs and which button asked.
// before
fn on_menu(mut requests: MessageReader<ShowContextMenu>) {
for request in requests.read() {
show_menu_for(request.entity);
}
}
// after
fn on_menu(mut requests: MessageReader<ContextMenuEvent>, mut signals: Signals) {
for request in requests.read() {
signals.open_menu_at("menu", request.position.x, request.position.y);
}
}
Scripts receive the same requests as the contextmenu DOM event.
The mouse cursor comes from CSS cursor¶
The pointer now shows the CSS cursor of the element under it, as in a
browser. Before, Lumen picked one of five cursors itself: the I-beam over text
fields, a pointing hand over anything clickable (a button, a toggle, a tab, an
element with a hover style or a tab-index), and grab hands over scrollbar and
slider thumbs.
Text fields keep the I-beam and links show the pointing hand, from the user-agent stylesheet. Everything else shows the platform arrow, the way native buttons and web buttons do, so buttons and other clickable elements stop showing the hand. To keep it, say so in your stylesheet:
Scrollbar and slider thumbs still show grab and grabbing.
For Rust code: lumen_core::input::CursorShape is gone. CursorRequest holds
a cursor (a Cursor: the url() images to try and a CursorKeyword) and the
decoded image, if any, instead of a shape. A window backend of your own shows
image when it is set and the keyword otherwise; CursorKeyword::None hides
the pointer.
@font-face loads its font on the desktop too¶
A @font-face rule used to reach only the web target; a desktop run ignored it
and drew with the fonts installed on the machine. The desktop now loads the
rule's font file from the app directory, so text naming its family draws with
it in lumenc run and in a packaged app as it does in a browser.
An app whose stylesheet already carries @font-face for its site changes look
on the desktop when the rule lists a TrueType or OpenType file. A rule that
lists only woff or woff2 files is left to browsers and the desktop keeps
the system font; add a ttf or otf beside them to use the font on both
targets.
linear-gradient angles paint the way CSS reads them¶
This affects desktop apps that use linear-gradient with an angle, or with no
angle. The desktop painted the angle turned a quarter: 90deg ran top to
bottom and the default 180deg ran right to left. It now paints what CSS
says and what a browser shows: 0deg bottom to top, 90deg left to right,
180deg top to bottom.
A gradient whose angle was chosen to look right on the old desktop now turns. Write the angle CSS means:
/* before: worked around the old desktop direction */
.panel { bg: linear-gradient(90deg, #202028, #101014); }
/* after: top to bottom, on the desktop and the web alike */
.panel { bg: linear-gradient(180deg, #202028, #101014); }
opacity fades an element and its children as one picture¶
An element with opacity below 1 and children now composites the whole
subtree first and fades the result once, as CSS does. Before, every descendant
faded on its own, so a parent's background showed through its opaque
children. A faded card with opaque content now looks the way it does in a
browser: the content hides the card under it. Nested opacities still compound.
No markup or CSS changes are needed; check any screenshot baseline that
captured a faded element with children.
This affects Rust plugins that paint native leaves too. NativeExtract::place
no longer takes the entity's Opacity; it reads opacity from the world itself
and NativePlacement::opacity is the alpha your leaf's own paints carry. A
faded ancestor with children is a layer the renderer composites over your
pixels, so it is no longer part of that figure, and NativePaintCtx::opacity
is 1.0.
Drop the Option<&Opacity> from the query that feeds place and the argument
from the call. Before and after:
A render backend built on lumen-paint has three more Painter methods
(begin_offscreen, end_offscreen, draw_offscreen), whose defaults paint
every faded group directly, and paint_frame takes the backend's LayerCache
as a fourth argument: pass Some from a sink that implements the three, None
otherwise. Code that builds a Node::Opacity by hand sets its new key, the
identity a backend keeps the group's layer under, or None.
Input messages carry a time_stamp¶
This affects Rust code that builds PointerData, MouseWheel, KeyPressed or
KeyReleased with a struct literal, typically a test that injects input. Each
now has a time_stamp: Instant field, the moment the event happened, so an
app can measure how long an input took to reach the screen (see
FrameTiming). PointerData::mouse stamps the current instant; a literal
names one.
// before
keys.write(KeyPressed { key, modifiers, repeat: false });
// after
keys.write(KeyPressed { key, modifiers, repeat: false, time_stamp: Instant::now() });
Code that only reads these messages is unaffected.
<input value> is the text a field starts with¶
This affects <input> elements written with a value attribute. value used
to be read as a number: a field written value="hello" failed to parse, and
one written value="5" started empty on the desktop while a browser showed
5. It is now the text the field starts with, as in HTML, on every target.
<!-- before -->
<input text="hello" />
<!-- after: either works -->
<input value="hello" />
<input text="hello" />
A field that wrote both keeps the one written last.
A signal written after the bindings ran paints on the next tick¶
This affects plugins and Rust systems that read the PropertyStore dirty
queue.
A signal written by a system the schedule ran after the bind-* readers used
to be cleared from the dirty queue at the end of its tick unseen, so the screen
kept the old value until some later input. Such a write now stays in the queue
into the next tick, which the run loop schedules at once, and the readers apply
it there. A DOM mutation queued from the C ABI or an SDK after the tick's DOM
commands were collected likewise schedules the tick that applies it.
A system that gates on dirty_peek can therefore see a key on the tick after
it was written as well as on the tick itself. A system that applies signal
writes and skips its work when none landed reads the queue with
PropertyStore::observe_dirty instead, so a write that lands after it is kept
for its next run.
<menubar> is drawn in the window on Linux¶
This affects apps that declare a <menubar> and run on Linux. It used to show
nothing there, because Linux desktops have no menu bar for an app to join. The
same markup now becomes a bar at the top of the window, its items reach
on_menu, and their accel shortcuts work. macOS and Windows keep the OS
menu bar.
An app that built its own in-window bar for Linux would now show two. Remove
the hand-built one, or drop the <menubar> on Linux:
<!-- before: a hand-built bar beside the declared one -->
<menubar>...</menubar>
<row class="my-linux-menubar">...</row>
<!-- after -->
<menubar>...</menubar>
The root's top padding grows by the bar's height, so the content starts below
the bar. A script reading the root's computed padding-top sees that sum.
The published lumenui links the release's prebuilt engine¶
This affects Rust SDK apps. lumenui no longer compiles the engine with your
app: its build script downloads the engine's compiled crates from the Lumen
release matching its version, and points the compiler at them. Three things
change for an app.
Pin the toolchain. The compiled crates are readable only by the rustc that
built them, so add a rust-toolchain.toml beside Cargo.toml with the channel
the release names. A build with another compiler stops and prints the file to
write:
Drop direct dependencies on the engine's crates. A dependency of your own on
bevy_ecs, glam, or any lumen-* crate now compiles a second copy whose
types do not match the engine's. Take them from lumenui instead:
// before: bevy_ecs = "0.19" in Cargo.toml, and
use bevy_ecs::prelude::Component;
// after: no extra dependency
use lumenui::prelude::*; // Component, Resource, Message, Bundle, Widget
let v = lumenui::glam::Vec2::ZERO;
Import the prelude with a glob (use lumenui::prelude::*): the bevy_ecs
derives find their crate through the bevy_ecs name it brings in.
Build on the machine you target: cross-compiling a lumenui app is refused.
In a Lumen checkout the SDK's code moved to the lumenui-engine crate in
sdk/rust (library lumenui_engine); sdk/lumenui is the crate apps depend
on. Something that depended on the checkout's sdk/rust by path keeps its
lumenui:: paths with
lumenui = { package = "lumenui-engine", path = ".../sdk/rust" }, and
cargo run -p lumenui --example counter is now -p lumenui-engine.
Text draws the same on the GPU and the CPU renderer¶
This affects apps and tools that compare screenshots of Lumen text against
saved images, and Rust code that implements a lumen_paint::Painter sink.
Glyph outlines are now flattened by Lumen, once per glyph and size, and both render backends fill the same polygons. Text edges therefore shift by a fraction of a pixel's coverage compared with earlier releases, closer to the true outline than before, and the GPU and CPU renderers draw them alike. Re-capture any screenshot baselines that include text.
A custom Painter sink that should draw text the way the built-in backends do
fills outline runs through lumen_paint::GlyphOutlines from its
draw_glyphs, and calls GlyphOutlines::end_frame once a frame:
use lumen_paint::kurbo::Rect;
use lumen_paint::{GlyphOutlines, GlyphRun, Painter};
fn draw_glyphs(&mut self, run: &GlyphRun<'_>) {
let mut outlines = std::mem::take(&mut self.outlines);
let visible = Rect::new(0.0, 0.0, self.width, self.height);
let drawn = outlines.draw(self, run, visible);
self.outlines = outlines;
if !drawn {
// A colour glyph face (COLR or bitmaps): draw the run natively.
}
}
A plugin added with add_plugin builds before the markup is parsed¶
This affects Rust apps that add plugins through the Rust SDK's
App::add_plugin or RunOptions::with_plugin. Those plugins now build right
after the app's runtime modules install, before the markup is parsed, where
they used to build after it. A widget plugin's tag is therefore registered in
time for the markup that uses it, with no register() call beforehand.
A plugin whose build read a resource the runtime adds while it sets up the
document, such as the scripting DOM's command queue, finds it missing now.
Read it from a system the plugin adds instead: systems run once the app is
built, when everything the runtime installs is there.
Pointer events carry the device¶
Pointer events now report the pen and touch readings the web PointerEvent
has, and a node can capture a pointer. Three existing behaviours change with
it.
A pointermove reports button as -1, as on the web; it used to report 0.
A handler that tells a drag from a hover by button() == 0 reads buttons()
instead, which holds the mask of buttons down.
The back and forward mouse buttons report 3 and 4; they used to report 0,
the primary button's code.
Rust code that writes PointerMoved, PointerPressed or PointerReleased
itself (a window backend, a test) fills in the new pointer field with the
device behind the event. PointerData::mouse(buttons) is the mouse, and
Default::default() the mouse with no button held. Code that reads presses to
drive a widget checks is_primary() rather than the button alone, so a second
finger does not press it.
// before
PointerPressed { position, button, local: None }
// after
PointerPressed { position, button, local: None, pointer: PointerData::mouse(1) }
The C ABI is 0.15: LumenEvent gains the pointer fields at its end and the
lumen_node_*_pointer_capture calls are new. The existing fields keep their
offsets, so a callback built against 0.14 reads them unchanged.
Rebuild every .lmna artifact and every compiler plugin¶
The compiled-app format moved on: a tag's defaults now travel as rules in the
artifact's stylesheet rather than as values in its tree, an element with a
custom widget tag keeps the attributes it was written with, an element
carries its CSS image-rendering, cursor and mask-* properties, and
an <input> keeps the type and alpha it was written with. The runtime
refuses an artifact written by an earlier toolchain, which would lose those
defaults the first time an element restyled and hand its widgets no
attributes.
This affects you if you load a precompiled app (lumenc run --artifact, a
.lmna written by lumenc build, or lumen_app_new_from_lmna from the C ABI
or an SDK), or if your app declares compiler plugins under [[plugins]].
Loading an old artifact fails with an error that says:
Rebuild the artifact with the new lumenc:
A compiler plugin records the same format version, so lumenc refuses one
built for the previous release; rebuild it against this release's
lumenc-plugin crate. A plugin that adds elements in transform_ir gives
them their styles with lumen_ir::css::apply_markup_declaration; see the
plugin guide.
Apps you already packaged carry their own runtime and keep working as they
are, as does a deployed site from lumenc web.
A restyle resolves an element from scratch, and tag defaults are rules¶
This affects apps that change classes, ids or inline styles from a script, apps that create elements from a script, and Rust code that drives an element's style components.
A restyle (a class added or removed, an id changed, a set_style, a theme
flip, a breakpoint crossing) used to copy over only the properties the new
resolution set, so a property that only a removed class had set stayed on the
element. It now resolves the element from scratch, as the first cascade did: a
property no rule, markup attribute or inline style sets any more goes back to
its initial value. Removing .selected from a row takes its justify and its
background with it.
What a tag is by default (a <column> stacking downwards, an <overlay>
covering its parent, a <spacer> taking the space left, a <scroll>
scrolling vertically) now comes from the built-in user-agent sheet instead of
from values the parser wrote into the element. An element a script creates
takes those defaults too: spawn("column") used to lay its children out in a
row, and now stacks them. A stylesheet rule for one of these tags applies from
the first frame on, where it used to take effect only once the app had
restyled. A scroll="x" attribute now lays its element's children out in a
row whatever the stylesheet says about flex-direction, like any other
attribute.
The metrics of the parts <tabs>, <menu>, <date-picker> and
<time-picker> expand into (a tab button's height and padding, a menu row's
height, a picker's width) moved into the same sheet. A skin's or an app's rule
for .tab-btn, .menu-item, .date-picker and the rest outranks them, as it
already did once the app had run its first frame.
Rust¶
A value a system writes into Style, Visuals, TextStyle or Opacity
behind the cascade's back is lost at the next restyle. Hold it in
lumen_core::style_overrides::StyleOverrides instead; see "Holding a style
value" in the plugin guide.
The snapshot components the built-in widgets kept are gone, because the layer
replaces them: HoverBaseColor, PressBaseColor, BaseBorder,
StateStyleBase, CheckboxBaseFill and RadioBaseFill, the default_top,
default_bottom and positioned fields of PopupPanel, and the
saved_display field of IfMarker. Read what an element shows under a
widget's layer with StyleOverrides::below.
In lumen_ir::css, reapply_single, reapply_single_with_media and
reapply_with_ancestors are replaced by RestyleWalk for walking a tree,
resolve_element for one element, and reapply_detached for an element not
yet in the tree. AncestorInfo carries an element's own declarations,
sibling list and content.
Before:
use lumen_ir::css::{AncestorInfo, SiblingPosition, reapply_with_ancestors};
let mut el = Element { tag: "tile".into(), ..Default::default() };
el.attrs.classes = vec!["card".into()];
reapply_with_ancestors(&mut el, &sheet, &media, &ancestors, SiblingPosition::default())?;
let attrs = el.attrs;
After:
use lumen_ir::css::{AncestorInfo, resolve_element};
let node = AncestorInfo::new("tile", vec!["card".into()], None)
.with_declarations(markup_and_inline_declarations);
let attrs = resolve_element(&node, &sheet, &media, &ancestors);
A packaged Rust app is one executable with the engine linked in¶
This affects Rust SDK apps. lumenc run, lumenc build and lumenc package
now build a Rust app with plain cargo, with no -C prefer-dynamic added to
RUSTFLAGS, so the engine is linked into the executable on every platform, as
the published lumenui already links it. lumenc package copies that
executable and the app's files; liblumen_engine and the Rust standard
library no longer travel beside it, and a package script that expected them
should stop looking for them.
A Rust app that declares a runtime module under [dependencies] in
lumen.toml is now refused by lumenc package on every platform, not only
Windows: a runtime module loads only into a shared engine, and a Rust app has
none. Portable plugins still travel.
lumen_source!("src/main.lmn", "src/main.css") now loads the app at the
directory above src/, so lumen.toml beside src/ is read in a debug
build. Before, a markup file under src/ made src/ itself the app
directory, and the run stopped with the "found code files at the app root"
error. Keep markup and stylesheet under src/ and point the macro there:
// before: markup beside the code, which the src/ layout rejects
lumen_source!("ui/main.lmn", "ui/main.css")
// after
lumen_source!("src/main.lmn", "src/main.css")
A release Rust app reads its files from the app, not the working directory¶
This affects Rust SDK apps built in release, and code that builds or matches
Source::Embedded by hand.
A release build embeds the entry markup and stylesheet, and reads everything
else (@imported stylesheets, @font-face fonts, url() images,
lumen.toml) from the app directory. That directory used to be the working
directory, so a release binary started anywhere but its crate directory lost
its imports, fonts, images and window settings. It is now the executable's
directory when that holds a lumen.toml (a packaged app), else the directory
lumen_source! embedded the sources from, and the working directory only when
neither exists.
A release binary that relied on being started from a directory holding a
different lumen.toml than its own now reads its own; pass that directory with
with_dir instead.
Source::Embedded has a third field, dir, the app directory the sources came
from. Source::embedded(markup, css) leaves it empty, and the new
Source::embedded_from(markup_file, markup, css) records the app directory of
markup_file. Code that matches the variant adds .. or the field:
// before
Source::Embedded { markup, css } => { /* ... */ }
// after
Source::Embedded { markup, css, .. } => { /* ... */ }
The Scoop bucket moved to the Lumen repository¶
This affects Windows users who installed Lumen with Scoop from the
lumen-fx/scoop-lumen bucket. The bucket is now the Lumen repository itself,
and new releases reach Scoop only there. The old bucket stays at the last
release it carried, so scoop update lumen stops finding anything newer.
Replace the bucket once, then update as before:
Lumen itself stays installed while the bucket is replaced, and the app name is
still lumen, so nothing else changes.
<split> is a built-in tag¶
This affects apps and runtime modules that use a tag named split of their
own: a custom widget registered as split, or a module that lists split
under tags. <split> is now the built-in container of resizable panes,
so a module declaring it is refused when it loads, as it is for every other
built-in tag, and markup that writes <split> gets the built-in panes and
gutters.
Rename your tag:
Or drop it and use the built-in one, which puts a draggable gutter between
its children; see <split> in the tags reference.
The tooltip popup takes the app's stylesheet¶
This affects desktop apps with a .tooltip rule or a tooltip type
selector, and apps whose root sets a font or a text color.
The popup a <tooltip> opens used to be styled by the runtime alone, so a
.tooltip rule in an app's stylesheet never reached it. It is now a
tooltip element with the class tooltip under the root, and the cascade
styles it like any element: an app's .tooltip rule restyles it over the
skin's look, and var() tokens declared on the root resolve in it. A rule
written for tooltips that did nothing before now applies.
The popup inherits from the root, so a font family set on the root reaches
tooltip text. Its default size stays 13 px, and its unskinned look (a dark
panel with light text) now comes from the built-in user-agent sheet, which
any skin or app rule for .tooltip outranks.
Rust¶
lumen_primitives::TooltipPopup entities now carry LumenTag, DomHidden
and a StyleOverrides that holds their position and inset, and are
children of the root of the trigger's tree. The runtime no longer inserts a
fallback Visuals and TextStyle on them. Code that wrote a popup's
Style, Visuals or TextStyle directly loses those writes at the next
restyle; hold the value in StyleOverrides instead.
Widget::spawn makes a given entity the widget¶
This affects Rust code that calls Widget::spawn itself or implements the
Widget trait by hand. An element written in markup with a
#[derive(Widget)] tag now becomes that widget: the spawner hands the
element and its attributes to Widget::spawn, which inserts the component on
it. So spawn no longer spawns an entity of its own; it takes the entity to
make the widget and returns nothing.
// before
let gauge = Gauge::spawn(parent, &attrs, world);
// after
let gauge = world.spawn_empty().id();
Gauge::spawn(gauge, &attrs, world);
A hand-written impl Widget changes its spawn the same way: insert the
component on the entity it is given.
// before
fn spawn(_parent: Entity, attrs: &Attributes, world: &mut World) -> Entity {
world.spawn(Gauge::from(attrs)).id()
}
// after
fn spawn(entity: Entity, attrs: &Attributes, world: &mut World) {
world.entity_mut(entity).insert(Gauge::from(attrs));
}
A hand-written impl Widget registered with register_widget_tag still only
makes its tag parse. Register it with lumen_widget::register_widget::<Gauge>()
for its elements to become the widget, as the derive's register now does.