Migrating to v0.0.9¶
archive::extract takes a fourth argument, opts¶
This affects scripts in apps that declare lumen-archive. extract now takes
an options value after the tag, whose include field lists glob patterns for
the files to keep. The argument is required, so a three-argument call no
longer compiles in candela and raises in Rhai and Lua.
Pass the defaults to keep the previous behaviour of writing every file.
candela, before and after:
archive::extract("themes.zip", "themes", "themes");
archive::extract("themes.zip", "themes", "themes", Default::default());
Rhai:
archive::extract("themes.zip", "themes", "themes");
archive::extract("themes.zip", "themes", "themes", #{});
Lua:
archive.extract("themes.zip", "themes", "themes")
archive.extract("themes.zip", "themes", "themes", {})
To keep only some files, name them, for example
archive::ExtractOptions { include: ["*.so"] } in candela. The pattern rules
are listed with archive::extract in each host's scripting reference.
Unset inset sides are auto, not 0¶
This affects apps that set position: absolute without naming every side of
inset. An unset side used to be 0, so an absolutely positioned element with
no inset stretched over its parent's padding box, and one pinned by only some
sides was held at 0 on the others. Unset sides are now auto, as the CSS
reference documents and as browsers do: the element keeps its content size
and sits against the sides you named.
To keep the stretch, name the sides:
<overlay> and <dialog> still default to inset: 0. inset also accepts
auto per side now, so inset: 4 8 auto auto pins an element to its parent's
top-right corner at its natural size.
The layout engine and the window backend come from registries¶
This affects Rust code that installs the layout engine or runs the window
itself: embedders that assemble their own App, custom window or layout
backends, and tests that add TaffyLayoutPlugin or WinitPlugin by hand.
Apps, scripts, and lumen.toml are not affected.
LayoutEngine and WindowBackend in lumen_core::traits are now traits a
launch drives, not markers. LayoutEngine::install(self: Box<Self>, app)
adds the engine's systems; the ones that write Transform go in the new
lumen_core::layout_backend::LayoutSolve set, which a system reading this
tick's boxes orders itself after. WindowBackend::run(self: Box<Self>, app,
options, renderers, a11y) runs the app until the window closes and returns
lumen_core::traits::WindowError, which replaces WinitError.
Each kind has a registry: LayoutBackends of LayoutBackend { name,
priority, engine } and WindowBackends of WindowBackendEntry { name,
priority, backend }, beside RenderBackends. All three are
lumen_core::backends::Backends<_>, and a backend registers with
lumen_core::backends::register_backend, which replaces
register_render_backend. The taffy engine and the winit backend register
themselves as the layout-taffy and window-winit capabilities in the new
Phase::Backends, which runs before the core stack.
TaffyLayout and WinitWindow are gone; TaffyLayoutPlugin is the taffy
engine and WinitBackend the winit one. lumen_window_winit::run is no
longer public. RedrawScheduler moves to lumen_core::window_backend, and
WinitPlugin is replaced by lumen_core::window_backend::WindowCorePlugin,
the window-free half every window backend shares. The accessibility bridge
reaches a window backend as an opaque lumen_core::traits::A11yBridgeFactory;
lumen_a11y_accesskit::bridge_factory() builds the one the winit backend
accepts, in place of lumen_a11y_accesskit::winit_bridge.
Before:
use lumen_core::render_backend::register_render_backend;
use lumen_layout_taffy::TaffyLayoutPlugin;
use lumen_window_winit::run;
app.add_plugin(TaffyLayoutPlugin);
app.add_systems(TickStage::LayoutSync, my_system.after(lumen_layout_taffy::sync_layout));
register_render_backend(&mut app, my_renderer);
let a11y: lumen_window_winit::A11yBridgeFactory = Box::new(lumen_a11y_accesskit::winit_bridge);
run(app, options, renderers, Some(a11y))?;
After:
use lumen_core::backends::register_backend;
use lumen_core::layout_backend::LayoutSolve;
use lumen_core::traits::WindowBackend;
use lumen_layout_taffy::TaffyLayoutPlugin;
use lumen_window_winit::WinitBackend;
app.add_plugin(TaffyLayoutPlugin);
app.add_systems(TickStage::LayoutSync, my_system.after(LayoutSolve));
register_backend(&mut app, my_renderer);
Box::new(WinitBackend).run(app, options, renderers, Some(lumen_a11y_accesskit::bridge_factory()))?;
Native painters draw through lumen_paint::PaintTarget¶
This affects Rust runtime modules and plugins that paint their own pixels
through the native-paint seam, with the paint feature of lumen-module.
The paint feature re-exports lumen_paint instead of lumen_render_wgpu,
and every render backend hands a painter a lumen_paint::PaintTarget, a
boxed Painter, instead of a vello::Scene. A painter that downcasts the
target to vello::Scene now finds nothing and draws nothing, and a module
that names lumen_module::lumen_render_wgpu no longer compiles. Draw through
the Painter trait instead; the same painter then draws on the GPU and on the
CPU renderer alike.
Before:
use lumen_module::lumen_render_wgpu::vello::Scene;
use lumen_module::lumen_render_wgpu::vello::kurbo::{Affine, Rect};
use lumen_module::lumen_render_wgpu::vello::peniko::{Color, Fill};
fn paint(&self, ctx: &mut NativePaintCtx<'_>) {
let transform = Affine::new(ctx.device_transform().coeffs);
let Some(scene) = ctx.target_as::<Scene>() else { return };
scene.fill(Fill::NonZero, transform, Color::WHITE, None, &Rect::new(0.0, 0.0, 8.0, 8.0));
}
After:
use lumen_module::lumen_paint::kurbo::{Affine, Rect};
use lumen_module::lumen_paint::peniko::{Color, Fill};
use lumen_module::lumen_paint::{PaintTarget, Shape};
fn paint(&self, ctx: &mut NativePaintCtx<'_>) {
let transform = Affine::new(ctx.device_transform().coeffs);
let Some(target) = ctx.target_as::<PaintTarget>() else { return };
target.fill(
Fill::NonZero,
transform,
Color::WHITE.into(),
None,
&Shape::Rect(Rect::new(0.0, 0.0, 8.0, 8.0)),
);
}
A drawing recorded ahead of the frame goes into a lumen_paint::Recording,
which the painter replays with recording.replay(&mut **target, transform).
on_click fires on every click, a double-click included¶
This affects scripts that define both on_click(id) and on_double_click(id)
for the same element, in every script language. A quick pair of clicks used to
call on_double_click alone, with neither click reaching on_click. Now both
clicks reach on_click and on_double_click runs once after the second, the
order a browser fires click and dblclick in. A counter, a "next" button, or
a row delete driven by on_click no longer loses the second of two fast clicks.
A script that relied on the old behaviour to give one element a single-click action and a different double-click action now runs the single-click action twice before the double-click one. Make the double-click action undo or supersede what the clicks did, or move the single action to a separate control.
A row that selects on a click and opens on a double-click keeps working as written in candela:
select now runs for both clicks, which is harmless when selecting is
idempotent; open still runs once.
Render backends implement one Renderer trait for windows and offscreen images¶
This affects Rust code that implements a render backend, calls one directly,
or reads the render world's damage list: embedders, custom window backends,
and tests that drive lumen-render-wgpu or lumen-render-cpu by hand.
lumen_core::traits::Renderer is now the whole renderer seam. It absorbs
SurfaceRenderer and OffscreenRenderer, which are gone, and a window and an
offscreen image are two kinds of FrameTarget the same renderer attaches to.
SurfaceError is renamed RenderError. RenderBackend carries one
constructor, renderer: fn() -> Box<dyn Renderer>, in place of surface and
offscreen. lumen_core::render_backend::install_offscreen puts an attached
renderer into an app and drives it each frame.
Each backend has one renderer type: WgpuSurfaceRenderer folds into
WgpuRenderer and CpuSurfaceRenderer into CpuRenderer. CpuRenderer::new
now builds a detached renderer; CpuRenderer::new_offscreen(width, height)
builds one attached to an image. WgpuRenderer::new_offscreen returns a
RenderError, and WgpuRenderer::adapter_info and WgpuRenderer::size
return Option, None while detached.
The FrameDamage resource and lumen_paint::damage_union are removed, and
lumen_paint::diff_retained_scenes returns whether the tree changed instead
of filling a damage list.
Before:
use lumen_core::render_backend::RenderBackend;
use lumen_core::traits::{SurfaceError, SurfaceRenderer};
let renderer: Box<dyn SurfaceRenderer> = (backend.surface)();
renderer.attach(window)?;
let offscreen = (backend.offscreen)(800, 600)?;
offscreen.install(&mut app);
let mut damage = FrameDamage::default();
diff_retained_scenes(prev, curr, viewport, &mut damage);
let changed = !damage.is_empty();
After:
use lumen_core::render_backend::install_offscreen;
use lumen_core::traits::{FrameTarget, RenderError, Renderer};
let mut renderer: Box<dyn Renderer> = (backend.renderer)();
renderer.attach(FrameTarget::Window(window))?;
let mut offscreen = (backend.renderer)();
offscreen.attach(FrameTarget::Offscreen { width: 800, height: 600 })?;
install_offscreen(&mut app, offscreen);
let changed = diff_retained_scenes(prev, curr, viewport);
OS capability events are plugin events, not core message types¶
This affects Rust code that reads or writes the events the OS capabilities
report: tray clicks, global hotkeys, notification buttons, file dialog
results, clipboard reads, recent-files and autostart reads, and second
launches of a single-instance app. Scripts are not affected; on_tray(id),
on("hotkey", name, fn) and the other handlers fire as before.
The message types TrayClicked, HotkeyFired, HotkeyReleased,
NotificationActionInvoked, FilePicked, ClipboardRead,
RecentFilesRead, AutostartRead and SecondInstanceLaunched are gone from
lumen_core::input, along with the re-exports lumen_os_tray::TrayClicked,
lumen_os_hotkey::{HotkeyPressed, HotkeyReleased},
lumen_os_notify::NotificationActionInvoked and
lumen_os_filedialog::FileDialogResult. Each capability now writes a
lumen_script::PluginEvent, built by a constructor the capability exports.
lumen_os_filedialog::FileDialogService::open and
drain_file_dialog_results are removed; use open_single and
FileDialogPlugin (or register_result_handler).
Before:
After:
The constructors are lumen_os_tray::tray_click_event,
lumen_os_hotkey::hotkey_event, lumen_os_notify::action_event,
lumen_os_lifecycle::{second_instance_event, recent_files_event,
autostart_event}, lumen_script::clipboard::clipboard_read_event, and
PluginEvent::from(FileDialogResultCommand). To observe these events from
Rust, read MessageReader<PluginEvent> and match on the event name.
process::start takes a fourth argument, opts¶
This affects scripts in apps that declare lumen-process. start now takes
an options value after the tag: the directory the child starts in (cwd),
variables to add to its environment (env), and whether to end it when the
app exits (end_at_exit). The argument is required, so a three-argument call
no longer compiles in candela and raises in Rhai and Lua.
Pass the defaults to keep the previous behaviour: the child starts in the app directory, inherits the environment, and outlives the app.
candela, before and after:
process::start("git", ["status"], "git");
process::start("git", ["status"], "git", Default::default());
Rhai:
Lua:
To set a field, name it and take the rest from the defaults, for example
process::StartOptions { cwd: "instances/a", ..Default::default() } in
candela. The options are listed with process::start in each host's scripting
reference. process::stop(tag) is new and ends a child you started.
lumen_script::push_plugin_event takes the event by value¶
This affects Rust runtime modules that deliver events to scripts through
lumen_module::lumen_script::push_plugin_event. The function now takes the
PluginEvent itself instead of a reference, so a call that passes &event
no longer compiles.
Before:
After:
Pass a clone if you still need the event after the call.
Rebuild every .lmna artifact¶
The compiled-app format moved on, and the runtime refuses an artifact written by an earlier toolchain. The format of the candela bytecode an artifact carries moved as well.
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. Loading an old one fails with an error that says:
Rebuild it with the new lumenc:
Apps you already packaged carry their own runtime and keep working as they
are; they pick up the new format the next time you run lumenc package. A
site from lumenc web is the same: a deployed one keeps working, and the next
build writes the new format.
Rebuild portable plugins and compiler plugins¶
Both kinds of native plugin record the engine version they were built against, and both of those versions moved in this release.
Portable plugins (the lumen-plugin cdylibs an app lists under
[dependencies], registry packages included) are checked against the script
wire version. It moved because an http() request now carries a
credentials option. A plugin built for the previous release fails its
handshake: the app prints a banner naming the plugin and starts without it.
Compiler plugins ([[plugins]] in lumen.toml, built with lumenc-plugin)
are checked against the compiled-app format, which moved as well, so lumenc
refuses one built for the previous release.
Rebuild each plugin against this release's lumen-plugin or lumenc-plugin
crate. If you publish one to the registry, publish the rebuilt library as a
new version.
ScriptHost::call returns a CallFailure when the function raises¶
This affects Rust code that implements lumen_script::ScriptHost for a script
language of its own, or calls call on a host directly. A failed call used to
return a bare ScriptError and drop the commands the function had queued; it
now returns CallFailure { error, commands }, and the runtime applies those
commands the way a browser keeps what a throwing listener did.
A host implementation drains its command sink on the error path too:
fn call(&mut self, name: &str, args: &[ScriptValue]) -> Result<CallOutcome, CallFailure> {
let result = self.run(name, args);
let commands = self.drain_commands();
match result {
Ok(ret) => Ok(CallOutcome { commands, found: ret.is_some(), ret }),
Err(error) => Err(CallFailure { error, commands }),
}
}
A caller that only wants the error reads failure.error.
Scripts need no change. A handler that raised used to lose the text it set, the timers it armed, and the lines it printed before the error; those now apply.
The script hosts are runtime modules¶
The engine no longer carries a script host. Each language runs on a runtime
module the toolchain ships: lumen-candela runs the bytecode a built or
packaged app carries, lumen-candela-dev compiles source for lumenc run,
check and build, and lumen-rhai and lumen-lua run the deprecated
languages. Lumen loads the module each script needs; an app declares nothing.
This affects you in these cases.
Your app is written in Rhai or Lua. It runs as before under lumenc run
and in a package, on every platform. lumenc package --static refuses it,
because a static executable carries no Rhai or Lua host, and a Linux or macOS
toolchain installed with --no-modules runs it without its script, with a
banner that says so. candela is the supported language.
You embed Lumen from Rust. RunOptions::rhai_extensions,
RunOptions::with_rhai_extension, lumen_runtime::run_with, lumenc::run_with,
AppBuilder::rhai_extension and the lumenui::rhai re-export are gone. Expose
a function with AppBuilder::native_fn or AppBuilder::script_fn (or
RunOptions::with_native_fn / a plugin's add_script_fn), which every
language reaches.
You select cargo features. The host-rhai, host-lua and host-candela
features are gone from lumen, lumen-runtime, lumenui, lumen-portable,
lumen-prerender, lumen-ssr and lumen-web-runtime; remove them from your
manifest. The crates lumen-script-candela, lumen-script-rhai and
lumen-script-lua are now lumen-candela plus lumen-candela-dev,
lumen-rhai and lumen-lua. A browser assembly installs
lumen_candela::CandelaPlugin itself before it hands a program to
lumen_portable::hosts::install.
You built a portable plugin. The plugin wire moved to version 4: a
function no longer says which languages see it (PluginFnBuilder::hosts and
lumen_script::HostSet are gone; every language sees every function).
Rebuild the plugin against this release; an older one is refused at load.
You load precompiled apps. The artifact format moved again: a candela
program now ships as bytecode alone, with the module that runs it named. See
"Rebuild every .lmna artifact".
You install with --no-modules. The script hosts are in the modules
archive, so a toolchain installed without it runs and builds no script.
You use the LSP's Rhai support. lumen-lsp's lang-rhai feature is now
off by default; build with --features lang-rhai to keep .rhai diagnostics.
Under navigation = "hard", page() loads a new document¶
This affects multi-page web apps that set [web] navigation = "hard" and move
between pages from a script.
Before, hard governed links only: a script's page() call still swapped the
page in place, with the app and its script state still running. Now hard
applies to scripts too. page() loads the target page as its own document, as
a new history entry, and page_back() and page_forward() step the browser's
history. Script state does not carry from one page to the next.
Desktop apps, single-file apps, and navigation = "soft" (the default) are
unchanged.
If a page relies on state a previous page set, either keep that state where
the next document can read it, for example with the lumen-storage module:
storage::set_item("draft", text); // before page("/next")
let draft = storage::get_item("draft"); // on the next page
or switch the site to soft navigation:
lumenc web --serve runs the separate lumen-server binary¶
lumenc web --serve no longer serves the site from inside lumenc. It starts
lumen-server --dev on the built site and waits for it. lumenc looks for
lumen-server beside itself, then at the path in $LUMEN_SERVER, then on
PATH, and fails naming those three places when none has it.
A release install (the install script, the MSI, Homebrew, Scoop, the AUR
package, or the setup-lumen action) puts lumen-server beside lumenc, so
nothing changes there.
A source install from cargo install lumenc does not build lumen-server.
Build it from a Lumen checkout of the same version and point LUMEN_SERVER at
it:
cargo build --release -p lumen-server
export LUMEN_SERVER="$PWD/target/release/lumen-server"
lumenc web myapp --serve
The --host, --port, and --allow-host flags work as before; lumenc
passes them on to lumen-server.
Wheel deltas on the desktop follow the DOM sign¶
A desktop wheel event's delta_y used to be negative when the wheel turned
toward the user, the opposite of the web target. It now follows the DOM on
every target: positive delta_y scrolls down and positive delta_x scrolls
right. A handler that zooms or steps through items by the wheel's sign goes the
other way on the desktop until you flip its comparison.
candela, before and after:
fn on_wheel(ev: int) { if event(ev).delta_y() < 0.0 { next_page(); } }
fn on_wheel(ev: int) { if event(ev).delta_y() > 0.0 { next_page(); } }
lumenc scroll and the lumen.simulate scroll kind take the same sign, so a
test that scrolled down with lumenc scroll 100 100 0 -50 now writes
lumenc scroll 100 100 0 50. A Rust plugin that writes or reads
lumen_core::input::MouseWheel uses the same convention.