Makepad apps on Android were randomly crashing within the `Cx::render_view`
callstack when the host Activity surface was recycled, e.g., on
background/foreground transitions, rotation, IME show/hide, etc.
Details of the change are generated below:
----
`SurfaceHolder.Callback.surfaceDestroyed` posted a fire-and-forget message
to the render thread and returned to Android immediately, leaving two
overlapping races:
1. The render thread could drain `SurfaceDestroyed` from the mpsc channel,
call `destroy_surface()` (which does `eglMakeCurrent(NULL, NULL, NULL,
NULL)` and nulls the EGL surface), then in the *same* `RenderLoop`
iteration call `handle_drawing()` → `render_view()` and issue GL calls
with no current EGL context.
2. Even when our Rust side was well-behaved, Android was free to recycle
the underlying buffer queue the moment `surfaceDestroyed` returned to
Java, while the render thread was still mid-frame against it.
**Layer 1 — Surface validity tracking.** Added `CxOs::surface_alive`,
flipped synchronously in `SurfaceCreated`/`SurfaceChanged`/`SurfaceDestroyed`
handlers, plus a `CxOs::has_drawable_surface()` helper that gates every
GL/Vulkan dispatch entry point: `main_loop`'s `handle_drawing()`,
`draw_pass_to_window_for_active_backend`, `draw_pass_to_texture_for_active_backend`,
`draw_pass_to_fullscreen`, and `eglSwapBuffers` in `present_window_for_active_backend`.
**Layer 2 — Synchronous Java↔Rust handshake.** `FromJavaMessage::SurfaceDestroyed`
now carries an `Arc<(Mutex<bool>, Condvar)>` ack channel. The JNI binding
blocks the Android UI thread on the condvar (2-second budget, well under
the 5-second ANR threshold) until the render thread confirms it has
released the surface. This is the same pattern `android.opengl.GLSurfaceView`
uses, and it closes the underlying-buffer-recycled-mid-frame race.
**Layer 3 — Defense-in-depth re-bind.** `draw_pass_to_fullscreen` now calls
a new fallible `try_make_current()` every frame, recovering from any GL
context drift caused by foreign code or our own teardown path, and
bailing cleanly if the bind fails instead of crashing inside the driver.
Verified all 5 gated entry points against the `openxr_render_loop` →
`openxr_handle_repaint` path:
- 4 entry points are unreachable in XR mode (XR uses its own swapchains
and `xrEndFrame`, never `eglSwapBuffers` or the popup overlay path).
- `draw_pass_to_texture_for_active_backend` IS reachable (off-screen UI
textures composited into the XR scene). To prevent these from being
wrongly skipped in Vulkan+XR mode after the host surface is recycled,
added an explicit XR escape hatch to `has_drawable_surface()`: when
`in_xr_mode && openxr.session.is_some()`, return `true` based purely on
`vulkan.is_some()`, ignoring the (intentionally nulled) `display.window`.
This matches the existing `keep_xr_backend_alive` logic in the
`SurfaceDestroyed` handler.
- `destroy_surface` is now idempotent (safe to call when already null).
- `make_current` asserts on null surface with a descriptive panic message
instead of crashing inside EGL.
- Lifecycle handlers verify surface creation actually succeeded before
flipping `surface_alive` to `true` (no false-positive ready signal).
Tested working on my OnePlus Open w/ Android 15.
-------------
* Other fixes: minor change to logging format to include level indicator
* Fix warning in cargo makepad android
The decoder only checked for double padding ('==') at input[len-2],
subtracting 1 output byte. Single padding ('=') at input[len-1] was
not handled, leaving 1 extra garbage byte in the decoded output.
This affected 2 out of 3 input lengths (any input where len % 3 == 2),
producing decoded output 1 byte longer than expected.
Fix: check input[len-1] for '=' first (subtract 1 byte), then check
input[len-2] for '=' (subtract another byte for double padding).
Added 7 roundtrip tests covering: no padding (3n bytes), single
padding (3n+2 bytes), double padding (3n+1 bytes), empty input,
lengths 1-20, all 256 byte values, and URL-safe alphabet.
Co-authored-by: prime intellect <prime@prime-intellects-Mac-Studio.local>
* Avoid re-entrant borrows of the IOS_APP global
I noticed this was happening any time the IME on iOS was used,
so I restructured those usages of IOS_APP to avoid them.
I then noticed that it could happen in other places, so I refactored
those as well.
* Fix missing iOS plist entry
* Fix `flow: Right` with `wrap: true`
This tiny math bug was causing widgets that got wrapped to the next line
in a `Flow: Right { wrap: true}` view to not get properly drawn
(the left side would get cut off).
* Expose `wrap_spacing` in `Layout` and splash script
This allows you to set the vertical spacing between widgets when
they wrap to the next line in a Right wrap flow layout.
Key repeat (holding down a key) did not work at all on Linux Wayland. The `RepeatInfo` event from the compositor was commented out, and all key events had `is_repeat: false` hardcoded.
- **`xkb_sys.rs`**: Added `xkb_state_get_keymap` FFI binding and a `key_repeats()` method on `XkbState` to check whether a key supports repeat (e.g., modifiers don't repeat).
- **`wayland_state.rs`**:
- Added `KeyRepeatState` struct and `KEY_REPEAT_TIMER_ID` constant.
- Added `key_repeat_rate`, `key_repeat_delay`, and `key_repeat` fields to `WaylandState`.
- Handled the previously-ignored `RepeatInfo` event to store the compositor's repeat rate/delay.
- On key press: start a one-shot timer with the repeat delay if the key supports repeat.
- On key release / keyboard leave: cancel the repeat timer.
- Added `handle_key_repeat_timer()` which fires `KeyDown(is_repeat: true)` and `TextInput` events, transitioning from the initial delay to a steady-state repeating timer.
- **`wayland_app.rs`**: Intercept the key repeat timer ID in the event loop and route it to `handle_key_repeat_timer()` instead of sending a generic `Timer` event.
- **`raw_input.rs`**: The evdev backend already received `KeyAction::KeyRepeat` from the OS but hardcoded `is_repeat: false` and didn't emit `TextInput` events. Fixed both.
- **`select_timer.rs`**: Fixed a pre-existing bug in `stop_timer` where removing a timer from the delta chain didn't adjust the successor's `delta_timeout`. This caused successor timers to fire early by the removed timer's delta. Also changed `update_timers` to use `pop_front()` instead of `stop_timer()` internally, since `select_time_used` already accounts for the removed timer's delta.
--------------
On Linux Wayland (and several other platforms), Makepad apps never received
`Event::Shutdown` when the window was closed via the client-side decoration
close button or when the app called `cx.quit()`.
When the CSD close button is clicked, it pushes `CxOsOp::CloseWindow`, which
is processed by `handle_platform_ops()`. When the last window is removed (or
`CxOsOp::Quit` is handled), this function returns `EventFlow::Exit`. However,
most backends did **not** call `Event::Shutdown` before exiting — only macOS
did it correctly.
Added `call_event_handler(&Event::Shutdown)` in the `handle_platform_ops() → Exit`
path for all affected backends, matching the existing macOS behavior:
- **Linux Wayland** (`linux_wayland.rs`) — added Shutdown call
- **Linux X11** (`linux_x11.rs`) — added Shutdown call
- **Windows** (`windows.rs`) — added Shutdown call
- **Linux Direct** (`linux_direct.rs`) — added Shutdown call
- **OpenHarmony** (`open_harmony.rs`) — added Shutdown call after main loop exit
(this backend uses `self.os.quit` instead of `EventFlow::Exit`)
- **macOS** — already correct
- **Android** — Shutdown is delivered via `FromJavaMessage::Destroy`
- **iOS / tvOS / Web** — different lifecycle models where explicit shutdown
doesn't apply (suspended by OS, or no reliable browser mechanism)
* cargo-makepad: support proper NDK builds (and on iOS)
The newly-emerging `aws-lc-rs` crate is quite popular and is
gradually replacing `ring`, which means we need to support it,
which is especially difficult to get right on Android and iOS targets.
This changeset makes it really easy to build that crate as part of
your app, if desired. There's no cost to apps that don't use it.
Android: make the stripped NDK installation the default, and make the
`full-ndk` option actually install the FULL NDK, not just the full set
of prebuilts. Like the whole thing, including build tooling like cmake
and other libraries.
* cargo-makepad: make install-toolchain for android more robust
Overwrite an existing installation instead of erroring out
* Support scrolling while centered, both vertically and horizontally
The previous turtle logic didn't allow you to center-align a view
while still making it scrollable. This small fix supports that now,
meaning that you can have:
* a vertically-centered view (`Align: { y: 0.5 }`) that is y-scrollable
* a horizontally-centered view (`Align: {x: 0.5 }`) that is x-scrollable
Also added some simple examples of this to `uizoo`
* fix iOS build
* Dock: add touch support for Tab/Tab bar interactions (scroll, drag/drop)
Details below:
1. **Finger-based tab drag-and-drop via long press** (tab.rs, android.rs, ios.rs):
- On touch devices, tab dragging now requires a long press before moving,
distinguishing it from scroll gestures.
- Added internal drag-and-drop support for Android and iOS backends,
synthesizing Drag/Drop/DragEnd events from touch move/up, matching the
existing Linux X11/Wayland approach.
2. **Finger-based drag-scrolling through the tab bar** (tab.rs, tab_bar.rs):
- A finger down + move (without long press) on a tab now scrolls the tab bar
horizontally instead of initiating a tab drag.
- Includes flick-to-scroll with velocity and decay for natural momentum.
- Touch tab selection is deferred to finger-up and only fires on a clean tap
(no long press, no scroll gesture), so scrolling/dragging doesn't
accidentally select tabs.
3. **Horizontal scroll input for tab bar** (scroll_bar.rs):
- When `use_vertical_finger_scroll` is enabled on a horizontal scroll bar,
both horizontal (trackpad) and vertical (mouse wheel) scroll inputs are
accepted, so trackpad users can scroll the tab list in either direction.
* fix drag/drop on macOS by using internal drag logic.
Fix ghost tab on dock to be much cleaner in terms of behavior
* Cleanup dock tab drag&drop behavior
Make platforms consistent. Switch macOS to internal drag item tracking
instead of OS-native (just for the dock for now).
Ensure ghost tab that gets drawn is consistently hidden (instantly)
upon being dropped in an invalid target zone.
* Smooth scroll the tab bar to the selected tab, if not visible
This animation does a lot to help the user track where tabs are.
Previously, without this, tabs would get lost in a lengthy tab bar
because you could select a tab and not realize which one was selected
as there was no visual indication that a non-visible tab was chosen.
This was especially strange when a new tab is programmatically selected,
as you couldn't tell where you were in the tab bar.
Now, if you select a tab that is far beyond the visible bounds of the
dock tab bar, it will auto-scroll to it with a smooth animation.
Also, if you click on a tab that is partially visible, it'll scroll
just enough to make that tab fully within the tab bar view.
Basically it's just like any IDE's tab bar now.
* Dock: add touch support for Tab/Tab bar interactions (scroll, drag/drop)
Details below:
1. **Finger-based tab drag-and-drop via long press** (tab.rs, android.rs, ios.rs):
- On touch devices, tab dragging now requires a long press before moving,
distinguishing it from scroll gestures.
- Added internal drag-and-drop support for Android and iOS backends,
synthesizing Drag/Drop/DragEnd events from touch move/up, matching the
existing Linux X11/Wayland approach.
2. **Finger-based drag-scrolling through the tab bar** (tab.rs, tab_bar.rs):
- A finger down + move (without long press) on a tab now scrolls the tab bar
horizontally instead of initiating a tab drag.
- Includes flick-to-scroll with velocity and decay for natural momentum.
- Touch tab selection is deferred to finger-up and only fires on a clean tap
(no long press, no scroll gesture), so scrolling/dragging doesn't
accidentally select tabs.
3. **Horizontal scroll input for tab bar** (scroll_bar.rs):
- When `use_vertical_finger_scroll` is enabled on a horizontal scroll bar,
both horizontal (trackpad) and vertical (mouse wheel) scroll inputs are
accepted, so trackpad users can scroll the tab list in either direction.
* fix drag/drop on macOS by using internal drag logic.
Fix ghost tab on dock to be much cleaner in terms of behavior
* Cleanup dock tab drag&drop behavior
Make platforms consistent. Switch macOS to internal drag item tracking
instead of OS-native (just for the dock for now).
Ensure ghost tab that gets drawn is consistently hidden (instantly)
upon being dropped in an invalid target zone.
* Add ellipsis text truncation support (text_overflow + max_lines)
Re-implement ellipsis truncation for text that overflows its container,
following conventions from CSS (text-overflow), Android (TextOverflow), and
Flutter (TextOverflow). This was supported in Makepad 1.0 but removed in 2.0.
Full summary below:
-----------------------
- Add `max_rows: Option<usize>` and `ellipsis: bool` fields to `LayoutOptions`
- Implement `apply_ellipsis_truncation()` post-processing step that:
- Detects when text was truncated (by max_rows or single-line overflow)
- Shapes the "…" (U+2026) glyph using the same font family
- Removes trailing glyphs from the last visible row to make room
- Trims trailing whitespace before the ellipsis for clean appearance
- Appends the ellipsis glyph(s) to the last row
- Recalculates the text bounding box
- Add early-exit in `layout_by_word()` and `layout_by_grapheme()` when
`max_rows` is exceeded, avoiding unnecessary layout work for long texts
- Add `is_truncated: bool` field to `LaidoutText` for consumer detection
- Fix pre-existing bugs in `LayoutOptions` Hash/PartialEq: `wrap` and
`line_spacing_scale` were missing, which could cause stale cache hits
- Add `TextOverflow` enum with `Clip` (default) and `Ellipsis` variants
- Add `max_lines: usize` and `text_overflow: TextOverflow` live properties
on `DrawText`, passed through to `LayoutOptions`
- Register `TextOverflow` in the script module for DSL access
- Resolve `Fit` width max bounds when ellipsis/max_lines is active, so
text layout knows the width constraint even for Fit-sized containers
- **Label** (widgets/src/label.rs): Add top-level `max_lines` and
`text_overflow` properties, forwarded to `draw_text` in `draw_walk()`
- **TextFlow** (widgets/src/text_flow.rs): Same top-level properties,
forwarded before each text draw call
- **Html / Markdown**: Inherit from TextFlow via `#[deref]` automatically
- Add `..mod.text` to widget prelude (widgets/src/lib.rs) so `Ellipsis`
and `Clip` are accessible in all widget DSL
- Make `FitBound::eval_width()` and `eval_height()` public (draw/src/turtle.rs)
so DrawText can resolve Fit max bounds during layout
```rust
// Simple single-line ellipsis
Label {
width: Fill
max_lines: 1
text_overflow: Ellipsis
text: "Long text gets truncated…"
}
// Multi-line with ellipsis
Label {
width: Fill
max_lines: 3
text_overflow: Ellipsis
text: "Wraps up to 3 lines, then truncates…"
}
// Also works via draw_text for any widget with DrawText
Button {
draw_text +: { max_lines: 1, text_overflow: Ellipsis }
}
```
See the demos I newly added to the `uizoo` example too.
* Fix TextFlow widget-level ellipsis for multi-run styled text (like Html)
The per-run forwarding of max_lines/text_overflow to DrawText caused each
styled run (bold, italic, etc.) to independently truncate with its own
ellipsis, producing double "……" artifacts in Html/Markdown content.
- Add `lines_drawn` and `content_truncated` fields to track visual lines
across all text runs within a single TextFlow
- Compute per-run `max_rows` based on remaining visual lines instead of
blindly forwarding the widget's `max_lines` to every DrawText call
- Handle continuation runs (starting mid-line) correctly: they get +1
row allowance since their first row shares the current visual line
- Skip further text runs once a run reports `is_truncated` (ellipsis drawn)
- Skip non-continuation runs when no visual lines remain
- `draw_walk_resumable_with` now returns `(usize, bool)`: row count and
whether the layout was truncated, so TextFlow can track state across runs
- Add three Html ellipsis examples: 1-line, 2-line styled, and emoji+styled
Details below:
1. **Finger-based tab drag-and-drop via long press** (tab.rs, android.rs, ios.rs):
- On touch devices, tab dragging now requires a long press before moving,
distinguishing it from scroll gestures.
- Added internal drag-and-drop support for Android and iOS backends,
synthesizing Drag/Drop/DragEnd events from touch move/up, matching the
existing Linux X11/Wayland approach.
2. **Finger-based drag-scrolling through the tab bar** (tab.rs, tab_bar.rs):
- A finger down + move (without long press) on a tab now scrolls the tab bar
horizontally instead of initiating a tab drag.
- Includes flick-to-scroll with velocity and decay for natural momentum.
- Touch tab selection is deferred to finger-up and only fires on a clean tap
(no long press, no scroll gesture), so scrolling/dragging doesn't
accidentally select tabs.
3. **Horizontal scroll input for tab bar** (scroll_bar.rs):
- When `use_vertical_finger_scroll` is enabled on a horizontal scroll bar,
both horizontal (trackpad) and vertical (mouse wheel) scroll inputs are
accepted, so trackpad users can scroll the tab list in either direction.
* TextInput: support single-line horizontal scrolling when text overflows
When a single-line TextInput's text content is wider than its visible area
(due to Fill, Fixed, or parent-constrained Fit width), the text now
automatically scrolls horizontally to keep the cursor visible.
- Add `scroll_x` tracking with auto-scroll-to-cursor logic
- Push a clip rect for all TextInput modes (not just multiline) to prevent
text from bleeding outside widget bounds
- Layout single-line text without max_width constraint so overflow is
detectable via `size_in_lpxs.width`
- Handle mouse wheel/trackpad scroll events for single-line horizontal
scrolling (maps both axes to horizontal)
- Account for `scroll_x` in IME position and selection rect calculations
- Add `Turtle::set_width()` and `Cx2d::compute_max_width_from_ancestors()`
(width counterparts to existing height methods)
- Add UIZoo examples: Fill width, Fixed width, and Fit-in-container
* a bit more cleanup, no need for clip size to be an option
* TextInput: support multiline mode with proper scrolling
* ScrollBar integration with mouse wheel, scrollbar handle drag,
and the correct way of dealing with `handled_y` propagation,
which basically means that scrolling can be handled by the TextInput
as a child, or not and left to the parent.
* Only auto-scroll to the cursor when it actually moves, not on every redraw
* `is_multiline` controls wrapping too, which makes more sense.
Now a single-line mode TextINput shouldn't wrap the text
* Allow setting `text` in the Splash DSL (make it `#[live]`)
Also added some examples to the uizoo demo: empty, pre-filled, read-only, toggle
between multi and single line.
* minor cleanup for TextInput multiline/scrolling
* Explicitly support multiline TextInput with Relative max height bounds
* more cleanup
* TextInput: support cascading parent-relative max height bounds
This was needed in Robrix, specifically a fairly common case in which
a TextInput should expand to Fit its content, but should not exceed
a certain percentage of the height of its parent.
This builds on the initial relative min/max Fit bounds that Eddy added
a while back, but weren't directly handled by TextInput, whcih itself
has special demands because it has to start internally
wrapping/scrolling.
* fix merge artifacts
* TextInput: support multiline mode with proper scrolling
* ScrollBar integration with mouse wheel, scrollbar handle drag,
and the correct way of dealing with `handled_y` propagation,
which basically means that scrolling can be handled by the TextInput
as a child, or not and left to the parent.
* Only auto-scroll to the cursor when it actually moves, not on every redraw
* `is_multiline` controls wrapping too, which makes more sense.
Now a single-line mode TextINput shouldn't wrap the text
* Allow setting `text` in the Splash DSL (make it `#[live]`)
Also added some examples to the uizoo demo: empty, pre-filled, read-only, toggle
between multi and single line.
* minor cleanup for TextInput multiline/scrolling
* Explicitly support multiline TextInput with Relative max height bounds
* more cleanup
Fixes a minor bug in which the entered text within a TextInput widget
would not get its layout recalculated if the width changed, meaning
that text could get cutoff on the right side of the widget.
Now we ensure that the text layout gets re-done (and the cached value
is not incorrectly used) if the width has changed since the last layout.