Compare commits

..

10 commits

Author SHA1 Message Date
7bddebb391 nigig: fix makepad-ai-hub ServiceHandle http_thread type (join network HttpServerHandle) 2026-09-03 03:29:46 +03:00
ca7bc8439b nigig: carry workspace.dependencies into android wrapper manifest
The generated android wrapper re-creates a standalone workspace and only
forwarded [patch.*] sections from the workspace root manifest, so deps
declared via [workspace.dependencies] + workspace = true failed to inherit
in wrapped crate builds. Extract the [workspace.dependencies] section the
same way patches are handled and inject it into the wrapper manifest.
2026-09-02 10:14:11 +03:00
282845b7e0 nigig: NIGIG test-mode forwarding, custom manifest hook, ortho camera support
- makepad_test/runtime.rs: forward NIGIG_TEST_MODE from host env to the
  Android app via 'am start' intent extra; add wait_timeout (60s) used by
  wait_visible/wait_hidden/wait_count; make query_widgets tolerant of
  snapshot timeouts; grant READ_CONTACTS during adb setup
- makepad-platform android_jni.rs: read makepad.NIGIG_TEST_MODE intent
  extra and surface it as the NIGIG_TEST_MODE env var via apply_studio_env
- cargo_makepad compile.rs: support verbatim custom AndroidManifest.xml in
  addition to the templated variant
- makepad-xr xr_root.rs: add ortho camera controls (ortho, ortho_height,
  min/max), derive Debug on XrCamera
- docs: ANDROID.md and DESKTOP_VISIBLE.md for makepad_test
2026-09-02 10:14:10 +03:00
318b480c0e makepad_test: grant runtime permissions after install to prevent dialog overlay
The GrantPermissionsActivity pops up during navigation and blocks the
app's event loop, preventing hub responses. Pre-grant all runtime
permissions after APK install to avoid this.
2026-09-02 10:14:10 +03:00
1f6e881880 makepad_test: force-stop interfering Robrix app during tests
PID 28203 (rs.robius.robrix) was the actual zombie reclaiming foreground
and killing our test app - not our own package. Force-stop both the
target package and known interfering Makepad apps (Robrix) during test
setup to prevent cross-app foreground competition.

Also remove the pm disable-user approach as it doesn't help against
a different package's zombie process.
2026-09-02 10:14:09 +03:00
26d831c891 makepad_test: use pm disable-user to prevent Samsung zombie resurrection
Samsung devices keep killed app processes alive and bring them back to
the foreground ~15s later, killing our fresh test instance. force-stop
and kill -9 don't prevent this. pm disable-user fully prevents the
zombie from being restarted. Re-enable before launching the new instance.
2026-09-02 10:14:09 +03:00
c17a21830e android: send BeforeStartup/AfterStartup over websocket
The Android platform never sent BeforeStartup or AfterStartup messages
via the studio websocket. Desktop platforms send these through their
stdin event loops, but Android uses websockets instead of stdin.

Without AfterStartup, the hub never broadcasts AppStarted to UI
clients, causing makepad-test to time out waiting for app startup.
2026-09-02 10:14:09 +03:00
debf970c36 feat(test): extend protocol for touch, long-press, paste, IME composition
Add RemoteTouchState, RemoteTouchPoint, RemoteTouchUpdate, RemoteLongPress,
RemoteTextPaste, RemoteIMEComposition wire structs to StudioToApp enum.

Dispatch new events through cx_shared.rs (TouchUpdate→Event::TouchUpdate,
LongPress→MouseUp+MouseDown, TextPaste/IMEComposition→Event::TextInput).

TestApp: touch_down/move/up, long_press, paste_text, ime_composition.
Locator: touch_down/move/up, long_press, paste, ime_composition.
2026-09-02 10:14:09 +03:00
757849940c feat(widgets): reexport optional Makepad sibling crates
Adds feature-gated optional deps and re-exports (makepad-test, makepad-csg,
makepad-gltf, makepad-mbtile-reader, makepad-fast-inflate) so a downstream
workspace can depend on makepad-widgets as its sole Makepad source.
2026-09-02 10:14:09 +03:00
9e7afd661e feat(test): Android makepad_test via adb + in-process hub (legacy Java path)
Adds the Android test runtime to makepad_test: builds the APK with
cargo-makepad's standard Java path, installs and launches via adb with
makepad.STUDIO_* intent extras (incl. STUDIO_BUILD), connects the app to an
in-process hub over adb reverse, and waits for startup + responsiveness.
Adds clean in-process hub shutdown (HttpServerHandle + GatewayHandle Drop)
and the STUDIO_BUILD intent parsing on the app side. No native-activity or
NDK APK compilation code is included.
2026-09-02 10:14:09 +03:00
7 changed files with 18 additions and 147 deletions

View file

@ -1,92 +0,0 @@
//! The opt-in switch for makepad's in-app developer overlays.
//!
//! Three of them exist, and each one binds a bare function key and then claims
//! input the app never sees:
//!
//! * **F10** — the exploded draw-list view ([`crate::sploded`]). Intercepted in
//! `Cx::call_event_handler` *before* the app's handler, and once it is up it
//! also claims Escape, the arrow keys, `+`/`-`/`0`, `I` and `H` — no modifier
//! required — plus every pointer drag outside the declared flat band.
//! * **F12** — the design tweaker (`makepad_widgets::tweaker`), a child of every
//! `Window`. Once it is up it swallows every pointer event over the body.
//! * **Shift+F12** — the screen recorder (`makepad_widgets::screen_cap`), which
//! writes mp4 files next to the running process.
//!
//! These are development tools, so they stay off unless a developer asks for
//! them. A shipped app is not a place to discover that a stray F10 tilts the
//! whole UI into 3D and stops Escape from closing anything.
//!
//! Turn them on with `--devtools` on the command line, or `MAKEPAD_DEVTOOLS=1`
//! in the environment. `--remote` implies them: the remote control surface's
//! `/snap` + `/click` loop drives the tweaker, so a remote-driven app has
//! already opted in to being instrumented. An explicit `MAKEPAD_DEVTOOLS=0`
//! wins over all of it, which is also how the off path stays testable under
//! `--remote`.
//!
//! Only the *hotkeys* are gated, not the tools. An app that wants one of these
//! on its own terms still calls `Cx::sploded_toggle`, `tweaker::set_tweak_on`
//! or `ScreenCap::toggle` directly — that is the app deciding, rather than a
//! key nobody knew was bound.
use std::sync::OnceLock;
/// Whether this process opted into the developer overlays.
///
/// Scans argv and the environment once and caches the answer, so the hot event
/// path pays an atomic load. See the module docs for what this gates.
pub fn enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
decide(
std::env::args().any(|a| a == "--devtools"),
std::env::var("MAKEPAD_DEVTOOLS").ok().as_deref(),
crate::remote::requested(),
)
})
}
/// The whole decision, with the process pulled out so it can be tested.
///
/// `MAKEPAD_DEVTOOLS` takes the usual off-ish spellings, so it can sit in a
/// shell profile as `0` instead of having to be unset — and because it is the
/// one explicit signal, an off spelling also overrides `--devtools` and
/// `--remote`.
fn decide(flag: bool, env: Option<&str>, remote: bool) -> bool {
if let Some(env) = env {
let env = env.trim().to_ascii_lowercase();
return !matches!(env.as_str(), "" | "0" | "off" | "false" | "no");
}
flag || remote
}
#[cfg(test)]
mod tests {
use super::decide;
#[test]
fn off_by_default() {
assert!(!decide(false, None, false));
}
#[test]
fn the_flag_or_remote_turns_it_on() {
assert!(decide(true, None, false));
assert!(decide(false, None, true));
}
#[test]
fn the_env_var_turns_it_on() {
for on in ["1", "yes", "true", "on", " 1 "] {
assert!(decide(false, Some(on), false), "{on:?} should enable");
}
}
#[test]
fn an_off_spelling_wins_over_the_flag_and_remote() {
// So `MAKEPAD_DEVTOOLS=0` can live in a profile, and so the gated-off
// path is still reachable under --remote.
for off in ["0", "off", "false", "no", "", " ", "OFF"] {
assert!(!decide(true, Some(off), true), "{off:?} should disable");
}
}
}

View file

@ -78,7 +78,6 @@ pub mod display_context;
#[macro_use] #[macro_use]
mod app_main; mod app_main;
pub mod remote; pub mod remote;
pub mod devtools;
pub mod pixel_probe; pub mod pixel_probe;
pub mod screen_capture; pub mod screen_capture;
pub mod audio_output_tap; pub mod audio_output_tap;

View file

@ -46,14 +46,11 @@ mod imp {
static ACTIVE: AtomicBool = AtomicBool::new(false); static ACTIVE: AtomicBool = AtomicBool::new(false);
/// True when this process asked for the remote bridge, in any of the forms /// True when this process was started with `--remote` (any form). Pure
/// [`requested_bind`] accepts — including `MAKEPAD_REMOTE`, which a plain /// argv scan, usable before the bridge itself is up — the platform's
/// argv scan used to miss, so `MAKEPAD_REMOTE=1` started the bridge while /// focus policy reads it while the first window is being created.
/// everything keyed off this said no. Pure argv + env, usable before the
/// bridge itself is up: the platform's focus policy reads it while the
/// first window is being created.
pub fn requested() -> bool { pub fn requested() -> bool {
requested_bind().is_some() std::env::args().any(|a| a == "--remote" || a.starts_with("--remote="))
} }
static NEXT_ID: AtomicU64 = AtomicU64::new(1); static NEXT_ID: AtomicU64 = AtomicU64::new(1);
static LIVE_CONNS: AtomicUsize = AtomicUsize::new(0); static LIVE_CONNS: AtomicUsize = AtomicUsize::new(0);
@ -461,14 +458,9 @@ mod imp {
/// "the user dismissed this" apart from "the app crashed", and remember it /// "the user dismissed this" apart from "the app crashed", and remember it
/// so requests aimed at that window get the real reason. /// so requests aimed at that window get the real reason.
pub fn note_user_closed_window(window_id: usize, title: &str) { pub fn note_user_closed_window(window_id: usize, title: &str) {
// Only chatter when the bridge is actually up: this line is for the
// agent driving the app, and a shipped app should not print
// `[makepad-remote] ...` to stdout every time a window closes.
let line = format!("[makepad-remote] user closed window {window_id} ({title:?})"); let line = format!("[makepad-remote] user closed window {window_id} ({title:?})");
if is_active() { println!("{line}");
println!("{line}"); let _ = std::io::stdout().flush();
let _ = std::io::stdout().flush();
}
push_log_line(line); push_log_line(line);
if let Ok(mut closed) = closed_windows().lock() { if let Ok(mut closed) = closed_windows().lock() {
if !closed.iter().any(|(id, _)| *id == window_id) { if !closed.iter().any(|(id, _)| *id == window_id) {
@ -481,10 +473,8 @@ mod imp {
/// away. Not a crash. /// away. Not a crash.
pub fn note_user_closed_last_window() { pub fn note_user_closed_last_window() {
let line = "[makepad-remote] app exit: user closed the last window".to_string(); let line = "[makepad-remote] app exit: user closed the last window".to_string();
if is_active() { println!("{line}");
println!("{line}"); let _ = std::io::stdout().flush();
let _ = std::io::stdout().flush();
}
push_log_line(line); push_log_line(line);
} }
@ -2138,10 +2128,6 @@ mod imp {
use crate::cx::Cx; use crate::cx::Cx;
pub fn start_if_requested() {} pub fn start_if_requested() {}
/// There is no remote bridge on these targets, so nothing ever asked for one.
pub fn requested() -> bool {
false
}
pub fn is_active() -> bool { pub fn is_active() -> bool {
false false
} }

View file

@ -11,8 +11,7 @@
//! scrollbar thumb by dragging is therefore deliberately NOT possible while //! scrollbar thumb by dragging is therefore deliberately NOT possible while
//! exploded — a drag is the orbit — and that is the coexistence rule. //! exploded — a drag is the orbit — and that is the coexistence rule.
//! //!
//! F10 — once the dev overlays are switched on ([`crate::devtools`]) — tilts //! F10 tilts the window into an isometric stack that renders **the component
//! the window into an isometric stack that renders **the component
//! nesting structure**: one plane per nesting level, siblings sharing a plane, //! nesting structure**: one plane per nesting level, siblings sharing a plane,
//! children lifting toward the viewer and their parents staying at the bottom //! children lifting toward the viewer and their parents staying at the bottom
//! of the stack. The point is to see — and click — the fully-covered parent //! of the stack. The point is to see — and click — the fully-covered parent
@ -728,12 +727,7 @@ impl Cx {
} }
match event { match event {
Event::KeyDown(e) => { Event::KeyDown(e) => {
// F10 is only ours when the app opted into the dev overlays if e.key_code == KeyCode::F10 {
// (`--devtools` / `MAKEPAD_DEVTOOLS=1` / `--remote`). Otherwise
// it is the app's key like any other. `sploded_toggle` still
// works either way, so an app can put the mode on a key of its
// own choosing.
if e.key_code == KeyCode::F10 && crate::devtools::enabled() {
if e.is_repeat { if e.is_repeat {
return true; return true;
} }

View file

@ -1,9 +1,5 @@
//! ScreenCap — SHIFT+F12 records the window to an mp4, picture and sound. //! ScreenCap — SHIFT+F12 records the window to an mp4, picture and sound.
//! //!
//! The hotkey needs the dev overlays switched on
//! (`makepad_platform::devtools`: `--devtools`, `MAKEPAD_DEVTOOLS=1`, or
//! `--remote`); an app that wants its own recording key calls [`ScreenCap::toggle`].
//!
//! One widget, hardcoded into [`crate::window::Window`] the way the tweaker //! One widget, hardcoded into [`crate::window::Window`] the way the tweaker
//! and the nav control are, so every Makepad app can record itself without //! and the nav control are, so every Makepad app can record itself without
//! wiring anything up. Shift+F12 starts, Shift+F12 stops. While it records, //! wiring anything up. Shift+F12 starts, Shift+F12 stops. While it records,
@ -47,7 +43,6 @@ use crate::makepad_draw::audio::AudioBuffer;
use crate::{makepad_derive_widget::*, makepad_draw::*, widget::*}; use crate::{makepad_derive_widget::*, makepad_draw::*, widget::*};
use makepad_platform::audio_output_tap::{add_audio_output_tap, remove_audio_output_tap}; use makepad_platform::audio_output_tap::{add_audio_output_tap, remove_audio_output_tap};
use makepad_platform::devtools;
use makepad_platform::screen_capture::{ use makepad_platform::screen_capture::{
add_screen_capture, remove_screen_capture, ScreenCaptureOptions, add_screen_capture, remove_screen_capture, ScreenCaptureOptions,
}; };
@ -326,12 +321,7 @@ impl ScreenCap {
impl Widget for ScreenCap { impl Widget for ScreenCap {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
if let Event::KeyDown(ke) = event { if let Event::KeyDown(ke) = event {
// The hotkey only exists once the dev overlays are switched on if ke.key_code == self.hotkey
// (`--devtools` / `MAKEPAD_DEVTOOLS=1` / `--remote`). A shipped app
// should not have a key that starts writing mp4s to disk; one that
// wants a recorder can call `toggle` from its own binding.
if devtools::enabled()
&& ke.key_code == self.hotkey
&& ke.modifiers.shift == self.hotkey_shift && ke.modifiers.shift == self.hotkey_shift
&& !ke.is_repeat && !ke.is_repeat
{ {

View file

@ -2653,8 +2653,12 @@ impl Widget for TextInput {
// In multiline mode, other modifier combos (Alt+Enter, or Ctrl+Enter // In multiline mode, other modifier combos (Alt+Enter, or Ctrl+Enter
// on macOS) insert a newline below when not read-only. // on macOS) insert a newline below when not read-only.
let has_physical_keyboard = cx.keyboard.has_physical_keyboard(); let has_physical_keyboard = cx.keyboard.has_physical_keyboard();
// Ctrl+Enter submits on every platform — on macOS `primary`
// is Cmd, and a person who reaches for Ctrl+Enter to send
// must not get a newline instead.
let should_submit = !self.is_multiline let should_submit = !self.is_multiline
|| mods.is_primary() || mods.is_primary()
|| mods.control
|| (has_physical_keyboard && self.submit_on_enter && !mods.any()); || (has_physical_keyboard && self.submit_on_enter && !mods.any());
if should_submit { if should_submit {
cx.hide_text_ime(); cx.hide_text_ime();

View file

@ -1,10 +1,7 @@
//! The TWEAKER — the design-feedback overlay every `--remote` app grows. //! The TWEAKER — the design-feedback overlay every `--remote` app grows.
//! //!
//! Hardcoded into `Window` (like the caption bar: zero app wiring), inert //! Hardcoded into `Window` (like the caption bar: zero app wiring), inert
//! unless the remote bridge is live, zero cost while off. The F12 key needs //! unless the remote bridge is live, zero cost while off. Turned on (F12 or
//! the dev overlays switched on (`makepad_platform::devtools`: `--devtools`,
//! `MAKEPAD_DEVTOOLS=1`, or `--remote`); [`set_tweak_on`] is always there for
//! an app that wants to open the panel itself. Turned on (F12 or
//! `GET /tweak?on=1`), a person points at the UI and live-edits it while the //! `GET /tweak?on=1`), a person points at the UI and live-edits it while the
//! AI watches the same session through the bridge: //! AI watches the same session through the bridge:
//! //!
@ -32,7 +29,6 @@
use crate::{ use crate::{
check_box::{CheckBox, CheckBoxAction}, check_box::{CheckBox, CheckBoxAction},
fab_controls::{format_hex, parse_hex, rgb_to_hsv, FabColorPick, FabColorPickAction, FabValueInput, FabValueInputAction}, fab_controls::{format_hex, parse_hex, rgb_to_hsv, FabColorPick, FabColorPickAction, FabValueInput, FabValueInputAction},
makepad_draw::makepad_platform::devtools,
makepad_draw::makepad_platform::sploded::{SPLODED_SPREAD_DEFAULT, SPLODED_SPREAD_MAX, SPLODED_SPREAD_MIN}, makepad_draw::makepad_platform::sploded::{SPLODED_SPREAD_DEFAULT, SPLODED_SPREAD_MAX, SPLODED_SPREAD_MIN},
file_tree::{FileTree, FileTreeAction}, file_tree::{FileTree, FileTreeAction},
label::Label, label::Label,
@ -620,19 +616,13 @@ pub fn window_intercept(
// F12 toggles the mode, bridge or no bridge: the design surface is // F12 toggles the mode, bridge or no bridge: the design surface is
// in-process and owes the remote nothing. Only the HTTP endpoints and // in-process and owes the remote nothing. Only the HTTP endpoints and
// the AI vibecode loop need --remote; without it they simply are not // the AI vibecode loop need --remote; without it they simply are not
// there, and the panel still is. It does need the dev overlays to be // there, and the panel still is.
// switched on though (`--devtools` / `MAKEPAD_DEVTOOLS=1` / `--remote`) —
// in a shipped app F12 belongs to the app, and `set_tweak_on` is still
// there for one that wants to open the panel itself.
// //
// SHIFT+F12 is not ours: that is the screen recorder // SHIFT+F12 is not ours: that is the screen recorder
// (widgets/src/screen_cap.rs), and it must not drag the design surface // (widgets/src/screen_cap.rs), and it must not drag the design surface
// into every recording. // into every recording.
if let Event::KeyDown(key_event) = event { if let Event::KeyDown(key_event) = event {
if key_event.key_code == KeyCode::F12 if key_event.key_code == KeyCode::F12 && !key_event.modifiers.shift {
&& !key_event.modifiers.shift
&& devtools::enabled()
{
let flip = { let flip = {
let mut s = session().lock().unwrap(); let mut s = session().lock().unwrap();
if s.toggle_event_id != cx.event_id() { if s.toggle_event_id != cx.event_id() {