`CharExt::column_count` was hard-coded to return 1 for every char. The
code editor's layouter advances x-position by this value per grapheme
(see `code_editor.rs:1217`), so CJK glyphs — which the text shaper draws
at ~2× the Latin monospace advance — overlap one another. Cursor
placement, selection rectangles, and wrap points suffer the same
off-by-half because they all read from `column_count`.
Match the Unicode East Asian Width property so Wide and Fullwidth
characters (plus common emoji that render at double-width) report 2
columns. Keeps a small literal match table instead of pulling in the
\`unicode-width\` crate, since only broad blocks are needed and perf on
the layout hot path matters.
Ranges covered:
U+3000..U+30FF CJK punctuation / Hiragana / Katakana
U+3400..U+4DBF CJK Unified Ideographs Extension A
U+4E00..U+9FFF CJK Unified Ideographs
U+AC00..U+D7AF Hangul Syllables
U+F900..U+FAFF CJK Compatibility Ideographs
U+FF00..U+FF60 Fullwidth forms
U+FFE0..U+FFE6 Fullwidth sign forms
U+20000..U+2FFFF CJK Unified Ideographs Extensions B..F
U+1F300..U+1F9FF Emoticons / symbols / transport / supplemental
Effect: Chinese/Japanese/Korean/emoji in code blocks render with correct
spacing in CodeView (and in any widget that layouts via CodeSession).
Latin-only workflows are unaffected.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent iOS touch-input correctness fixes.
1. Hit-test: stop inflating widget bounds by touch radius
`Cx::hits_with_options_and_test` was inflating every widget's
clickable rect outward by `touch.radius` on all four sides, where
`radius` comes from `UITouch.majorRadius` on iOS (and the Android
equivalent). On real fingers this is 5-15pt, but the iOS Simulator
synthesizes mouse clicks as touches with `majorRadius` ~25-40pt, so
every button in the simulator captured clicks ~30pt outside its
visible bounds — a click well above or below the Upload Avatar
button still activated it.
Remove the inflation in both TouchState::Start and TouchState::Stop
and hit-test against the touch centroid only. This matches UIKit/
AppKit native behavior; Apple's HIG and Material Design already
require touch targets large enough that hidden inflation isn't
needed. Apps that want explicit hit padding can still pass it via
`HitOptions::margin`. `touch.radius` is still populated and
delivered to apps that want it for visual feedback.
Affected platforms: iOS, Android, web touch. Desktop mouse paths
(MouseDown/Up) never touched this code.
2. Window geometry: read from MTKView, not UIScreen
`IosApp::check_window_geom` was reading `inner_size` from
`UIScreen.mainScreen.bounds`, which describes the *physical screen*
rather than the app's drawing surface. On iPad Split View, Slide
Over, Stage Manager, and multi-scene apps the window is a fraction
of the screen, so `inner_size` could disagree with the MTKView's
actual bounds — introducing a constant offset between layout and
the touch coordinate space (UITouch `locationInView:` is always
view-local).
Read `bounds`, `contentScaleFactor`, and `safeAreaInsets` from the
MTKView itself, falling back to UIScreen only during the early-init
window before the view exists.
Also use the `size` parameter delivered to
`mtkView:drawableSizeWillChange:` directly (converting pixels→points
via `contentScaleFactor` from the same view) rather than re-querying.
UIKit gives us the authoritative new size synchronously with the
resize callback; re-reading any other source can race by one layout
pass during rotation or multitasking transitions.
Extract the common WindowGeom construction / first-draw / update_geom
/ callback-dispatch tail into `apply_new_window_geom`.
Shaping a string containing right-to-left characters (Arabic, Hebrew,
etc.) could panic in shape_recursive with "byte range starts at N but
ends at M", and even when it didn't panic, the surrounding LTR text was
laid out in visually wrong positions.
- shaper: compute fallback byte ranges in shape_recursive in a
direction-aware way. HarfBuzz emits clusters monotonically in the
shaping direction (non-decreasing for LTR, non-increasing for RTL),
so the "next logical cluster" lives visually after the current group
for LTR and visually before it for RTL. Also collect glyph groups
into an indexable Vec so we can look both forward and backward, and
defensively fall back to rendering .notdef tofu rather than
recursing on an inverted range if HarfBuzz ever produces unexpected
non-monotonic output.
- shaper: run the Unicode Bidirectional Algorithm via the already-
present unicode-bidi crate before shaping. Segment each input into
visual runs with ParagraphBidiInfo, then shape each run in its
resolved direction and append in visual order. This keeps an RTL
chunk embedded in LTR text from stomping on the surrounding LTR
layout. Full RTL support (joining quality, cursor/hit-testing in
RTL runs) is still not wired up above the shaper.
- shaper: add an is_definitely_ltr fast path that short-circuits BiDi
entirely when the input contains no characters in any RTL Unicode
block. Uses str::is_ascii (SIMD) for the common ASCII case and
falls back to a char-level range check, so ASCII / Latin / Greek /
Cyrillic / CJK / emoji text pays no BiDi classification or vec-
allocation cost on cache miss.
- shaper: cache the rustybuzz Feature conversion in shape_step with
a one-slot content-keyed cache, so repeated shape calls with the
same feature set (typically empty) don't rebuild the Vec each time.
Implement streaming-compatible table support in the shared TextFlow
layout engine. Tables render incrementally as events arrive — no
buffering needed since pulldown_cmark provides column count upfront
via the header separator line.
- Add FlowBlockType::TableCell variant with SDF shader for cell
borders and header backgrounds in DrawFlowBlock base definition
- Add begin/end table, row, and cell methods to TextFlow using
turtle layout with equal-width column distribution
- Draw cell borders after row layout completes so all cells share
the row's max height for uniform borders
- Replace 8 TODO stubs in markdown.rs with direct TextFlow calls
- Add table/thead/tbody/tr/th/td tag handling in html.rs with
column counting via HtmlNode lookahead
- Skip whitespace-only text nodes inside HTML tables using the
pre-computed all_ws flag
* Android: fix nondeterminstic crashes when using a bad surface
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
* Fix Android platform errors, mostly no draw on start/resume
Also a small related optimization on iOS
------------
* Android: JNI method ID caching (`ndk_utils.rs`)
* Rewrote the `call_method!` macro to cache `jmethodID` in a per-call-site `static AtomicPtr`. Previously, every JNI call allocated two `CString`s and called `GetObjectClass` + `GetMethodID` from scratch. Now these are resolved once and reused for all subsequent calls. Also deletes the local class reference after first resolution.
* Android: `to_java_update_tex_image` uses cached macro (`android_jni.rs`)
* Replaced manual `GetObjectClass`/`CString::new`/`GetMethodID` calls with the now-cached `call_bool_method!` macro, eliminating per-frame JNI overhead for video texture updates.
* Android: Touch event coalescing (`android.rs`)
* When draining pending messages before a RenderLoop frame, consecutive pure-Move touch events are now coalesced — only the last position is dispatched. Start/Stop events are never dropped. This reduces redundant event dispatch during active touch scrolling.
* Android: Black screen fix (`android.rs`)
* Added `needs_first_draw` flag to `CxOs`. When a `RenderLoop` callback arrives but the surface isn't drawable, the flag is set. When the surface later becomes available, `redraw_all()` is called to ensure the first frame is painted. The flag is also set on `SurfaceDestroyed` so resuming from background always gets a guaranteed first frame.
* iOS: Remove unnecessary `passes_todo.clone()` (`ios.rs`)
* Removed a redundant `Vec::clone()` in the popup pass draw loop — both the outer and inner loops borrow `passes_todo` immutably.
* Optimize text layout and PortalList widget performance
Text layout: eliminate redundant HarfBuzz reshaping (layouter.rs)
- Replace `can_fit()` reshaping of cumulative multi-word substrings (always cache misses) with summing pre-computed per-word widths
- Replace `fit()` reshaping with concatenation of cached per-word `ShapedText` results, adjusting cluster offsets
- Cache each word's `Rc<ShapedText>` in the Fitter constructor for reuse
PortalList: O(1) reusable item pool lookup (portal_list.rs)
- Change `reusable_items` from `Vec<WidgetItem>` (O(n) scan + O(n) remove) to `HashMap<LiveId, Vec<WidgetItem>>` (O(1) lookup + O(1) pop)
PortalList: skip touch cursor hit-testing (portal_list.rs)
- Gate `point_hits_interactive_item()` behind `!e.device.is_touch()` to skip expensive recursive widget-tree walk on touch devices where there is no cursor
* undo unnecessary change to inter-word/ligature width estimation
avoid problems with words'/ligatures' kerning
The font_member_is_needed_for_text() optimization pre-scanned text content
to decide whether to skip loading CJK/emoji font members. This was broken
in two ways:
1. The hardcoded Unicode ranges in is_emoji_char() were incomplete (e.g.,
missing U+2B50 ⭐), so emoji glyphs silently failed to render.
2. When members were skipped, font_ids.len() never matched
expected_member_count, so is_font_family_complete() never returned true
for i18n font families. This caused every draw call to hit the slow
path — the opposite of the optimization's intent.
Fix: load all font family members unconditionally. The one-time load cost
is negligible, and the fast path (is_font_family_complete → early return)
now works correctly on subsequent draws.
Removed the now-unused helpers: font_member_is_needed_for_text(),
is_cjk_fallback_font_path(), is_emoji_fallback_font_path(),
resource_basename(), text_has_cjk(), is_cjk_char(), text_has_emoji(),
is_emoji_char(), and FontFamily::ensure_fonts_loaded_for_text().
* Fix emojis in Html by removing broken font loading optimization
The font_member_is_needed_for_text() optimization pre-scanned text content
to decide whether to skip loading CJK/emoji font members. This was broken
in two ways:
1. The hardcoded Unicode ranges in is_emoji_char() were incomplete (e.g.,
missing U+2B50 ⭐), so emoji glyphs silently failed to render.
2. When members were skipped, font_ids.len() never matched
expected_member_count, so is_font_family_complete() never returned true
for i18n font families. This caused every draw call to hit the slow
path — the opposite of the optimization's intent.
Fix: load all font family members unconditionally. The one-time load cost
is negligible, and the fast path (is_font_family_complete → early return)
now works correctly on subsequent draws.
Removed the now-unused helpers: font_member_is_needed_for_text(),
is_cjk_fallback_font_path(), is_emoji_fallback_font_path(),
resource_basename(), text_has_cjk(), is_cjk_char(), text_has_emoji(),
is_emoji_char(), and FontFamily::ensure_fonts_loaded_for_text().
* Ensure .notdef glyph for unsupported characters is visibly drawn
When a character isn't in any font in the family (e.g., U+1FAEA not in
the bundled NotoColorEmoji), the shaper exhausts all fallback fonts and
emits glyph id 0 (.notdef) from the last font tried. For bitmap-only fonts
like NotoColorEmoji (no glyf table), the .notdef has no outline, so the
rasterizer returns None and draw_glyph silently skips it — leaving
invisible blank space.
Fix: in shape_recursive, when all fallback fonts are exhausted and glyphs
remain unmapped (id == 0), reassign them to the primary font (IBM Plex
Sans), whose .notdef has visible contours (the standard "tofu" rectangle).
This is done at the point of emission rather than as a post-processing
scan, so the common case (all glyphs found) has zero overhead.
* Another proper fix for black screen on Android start/resume
* Fix all Android surface artifacts on pause, resume, and cold start
* cleanup: remove java-level logging in MakepadActivity
* additional cleanup/improvements for android lifecycle stuff
* WIP fixing performance issue on Windows OS when resizing app window
* Improved resize behavior (flickering/stretching) on Windows
* cleanup window-resize optimizations for Windows OS
* Android: fix nondeterminstic crashes when using a bad surface
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
* Fix Android platform errors, mostly no draw on start/resume
Also a small related optimization on iOS
------------
* Android: JNI method ID caching (`ndk_utils.rs`)
* Rewrote the `call_method!` macro to cache `jmethodID` in a per-call-site `static AtomicPtr`. Previously, every JNI call allocated two `CString`s and called `GetObjectClass` + `GetMethodID` from scratch. Now these are resolved once and reused for all subsequent calls. Also deletes the local class reference after first resolution.
* Android: `to_java_update_tex_image` uses cached macro (`android_jni.rs`)
* Replaced manual `GetObjectClass`/`CString::new`/`GetMethodID` calls with the now-cached `call_bool_method!` macro, eliminating per-frame JNI overhead for video texture updates.
* Android: Touch event coalescing (`android.rs`)
* When draining pending messages before a RenderLoop frame, consecutive pure-Move touch events are now coalesced — only the last position is dispatched. Start/Stop events are never dropped. This reduces redundant event dispatch during active touch scrolling.
* Android: Black screen fix (`android.rs`)
* Added `needs_first_draw` flag to `CxOs`. When a `RenderLoop` callback arrives but the surface isn't drawable, the flag is set. When the surface later becomes available, `redraw_all()` is called to ensure the first frame is painted. The flag is also set on `SurfaceDestroyed` so resuming from background always gets a guaranteed first frame.
* iOS: Remove unnecessary `passes_todo.clone()` (`ios.rs`)
* Removed a redundant `Vec::clone()` in the popup pass draw loop — both the outer and inner loops borrow `passes_todo` immutably.
Windows was issuing two shutdown events any time the window was closed,
so it no longer does that. Might've been my fault in a previous change,
not sure.
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