Commit graph

1,863 commits

Author SHA1 Message Date
Alex
03a64a9406
fix(code_editor): honour East Asian Wide width in column_count (#1048)
`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>
2026-04-16 22:00:54 +02:00
Kevin Boos
293804f044
Fix iOS touch hit-test and window geometry source (#1047)
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`.
2026-04-16 20:51:13 +02:00
Kevin Boos
93a5a875f3
Fix RTL/bidi text shaping panic and mis-layout (#1046)
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.
2026-04-16 20:51:02 +02:00
Admin
6c8d210d9d Clean up MLX backend boundary config 2026-04-16 19:07:04 +02:00
Admin
12a0e1b135 Add CUDA multimodal exact path 2026-04-15 21:33:39 +02:00
Admin
4a562a2b51 Fix CUDA chat repeated prompt loop 2026-04-15 21:33:39 +02:00
Admin
eba806b819 Add CUDA exact chat windowing and kernels 2026-04-15 21:33:39 +02:00
poborin
51056831c5
Add table rendering to TextFlow engine for Markdown and Html widgets (#1032)
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
2026-04-15 11:48:14 +02:00
Kevin Boos
6102d4329b
Optimize text layout and PortalList widget performance (#1041)
* 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
2026-04-15 11:47:47 +02:00
Kevin Boos
e66d1041cd
Fix emojis in Html by removing broken font loading optimization (#1044)
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().
2026-04-15 10:59:49 +02:00
Kevin Boos
007bf000a1
Ensure .notdef glyph for unsupported characters is visibly drawn (#1045)
* 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.
2026-04-15 09:28:21 +02:00
Kevin Boos
0136260f6a
Fully proper fix for black screen on Android start/resume (#1043)
* 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
2026-04-15 08:22:35 +02:00
Kevin Boos
3cf602a432
Windows: fix perf and flicker/stretch artifacts when resizing a window (#1036)
* 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
2026-04-13 21:51:22 +02:00
Kevin Boos
113ab5c6ad
Fix Android platform errors, mostly no draw on start/resume (#1035)
* 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.
2026-04-13 21:51:09 +02:00
Kevin Boos
ccecc172e9
Avoid double-shutdown Event dispatch on WIndows (#1034)
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.
2026-04-13 21:50:55 +02:00
Admin
c9ada286a3 cuda otw 2026-04-13 11:09:08 +02:00
Admin
56d948d62f cuda exact chat reuse and decode tuning 2026-04-13 11:09:08 +02:00
Admin
7b81d7ef6b wip cuda long-context profiling 2026-04-13 11:05:49 +02:00
Admin
ceb1e384e8 cuda prefill metrics and chunked kernels 2026-04-13 11:05:49 +02:00
Admin
1ab6e6ed27 cuda exact prefill to ~87 tok/s 2026-04-13 11:05:49 +02:00
Admin
4b801ae74e Add FLUX warm pipeline and reference benchmarks 2026-04-13 11:01:13 +02:00
Admin
dc3baaf69d flux 1 works 2026-04-13 11:01:13 +02:00
Admin
3335c26f27 rotor quant for metal 2026-04-13 11:01:13 +02:00
Admin
93c013707a Document Rotor divergence against BF16 baseline 2026-04-13 11:01:13 +02:00
Admin
cc480e274e Add optional Rotor-style K-cache compression for Gemma 4 2026-04-13 11:01:13 +02:00
alanpoon
985cb4adb6
audio_input_panic_fix for macos (#1038) 2026-04-11 13:52:25 +02:00
Admin
b262f7bac3 Add CUDA NVFP4 backend and GPU decode path 2026-04-11 11:44:04 +02:00
Admin
d7520e837f mlx image opt 2026-04-11 09:51:37 +02:00
Admin
f1a2bec527 fix sutdio 2026-04-10 22:06:51 +02:00
Admin
3a90346eaa fix 2026-04-10 21:14:32 +02:00
Admin
5e7955770e mlx multimodal 2026-04-10 15:10:25 +02:00
Admin
4bb2190ea8 tabs 2026-04-10 14:48:17 +02:00
Admin
b12af23a7a mlx optimisations 2026-04-10 14:48:17 +02:00
Admin
ffc578c243 mlx working 2026-04-10 14:48:17 +02:00
Admin
32612cfe88 cleanup 2026-04-10 14:48:17 +02:00
Admin
c925d2a56b mlx otw 2026-04-10 14:48:17 +02:00
Admin
747ef9b2e8 mlx 5x->2x 2026-04-10 14:46:22 +02:00
Admin
db323ee42e mlx 2026-04-10 14:46:22 +02:00
Admin
fc5d960062 mlx otw 2026-04-10 14:46:22 +02:00
Admin
087952a638 mlx otw 2026-04-10 14:46:22 +02:00
Kevin Boos
126e848063
Android: fix nondeterminstic crashes when using a bad surface (#1030)
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
2026-04-10 08:13:14 +02:00
Kevin Boos
92006e12e3
Batch/buffer tooltip hover in/out actions to avoid flicker (#1029) 2026-04-10 08:12:50 +02:00
Ruben Daniels
ebe1cab1b7
fix(base64): handle single '=' padding in base64_decode (#1027)
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>
2026-04-09 20:05:45 +02:00
Kevin Boos
3d81877bf4
Avoid re-entrant borrows of the IOS_APP global (#1025)
* 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
2026-04-09 14:45:37 +02:00
Kevin Boos
d12fa28a8e
Packaging directory-related fixes (for robius-packaging-commands to work) (#1024)
* Packaging directory-related fixes (to allow robius-packagin-commands to work)

* use proper resource loading for macOS packaged apps via NSBundle
2026-04-09 14:45:23 +02:00
Kevin Boos
7c25ded861
Flow right wrap fix (#1023)
* 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.
2026-04-08 07:36:19 +02:00
Kevin Boos
7b5b05fb29
Fix mouse wheel scrolling on Linux Wayland (#1022)
Now the tab bar will scroll using all four mouse wheel directions
even on Wayland.
2026-04-08 07:36:04 +02:00
Kevin Boos
2374359993
Fix key repeat timer issue, and Event::Shutdown delivery (#1020)
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)
2026-04-08 07:35:47 +02:00
Kevin Boos
5e7d2a45a2
cargo-makepad: support builds that need cmake/bindgen (Android/iOS) (#1019)
* 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
2026-04-08 07:35:29 +02:00
Kevin Boos
cc60726b35
Support scrolling while centered, both vertically and horizontally (#1017)
* 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
2026-04-07 09:32:47 +02:00