Compare commits

..

11 commits

Author SHA1 Message Date
531114e841 nigig: fix makepad-ai-hub ServiceHandle http_thread type (join network HttpServerHandle) 2026-09-03 10:55:00 +03:00
5ec5b65e8d 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-03 10:55:00 +03:00
477421e314 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-03 10:55:00 +03:00
aeca2e551a 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-03 10:55:00 +03:00
454d60897e 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-03 10:54:59 +03:00
5c99c410b1 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-03 10:54:59 +03:00
bc55ff94aa 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-03 10:54:59 +03:00
dae59f1422 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-03 10:54:59 +03:00
3e6c5fd57e 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-03 10:54:59 +03:00
4c94531456 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-03 10:54:58 +03:00
Kevin Boos
493d23a763
Put the F10/F12/Shift+F12 dev overlays behind an opt-in (#1209)
* remote: honor MAKEPAD_REMOTE in requested(), and hush the close notices when the bridge is off

`requested()` only scanned argv, while `requested_bind()` also reads
MAKEPAD_REMOTE. So `MAKEPAD_REMOTE=1` started the bridge but everything
keyed off `requested()` still said no. Just delegate, so there's one
answer to "did this process ask for the remote bridge".

Also stop printing `[makepad-remote] user closed window ...` to stdout
from every app on every window close -- that line is for the agent
driving the app, so only print it when the bridge is actually up. The
log ring still gets it either way, for /log.

* devtools: put the F10/F12/Shift+F12 overlays behind an opt-in

Three dev tools are currently wired into every app with no way to turn
them off, each on a bare function key:

  F10        the exploded draw-list view. Intercepted in
             Cx::call_event_handler *before* the app's handler, and once
             it's up it also eats Escape, the arrows, +/-/0, I and H --
             no modifier needed -- plus every drag outside the flat band.
  F12        the design tweaker, a child of every Window. Once it's up it
             swallows every pointer event over the body, so the app looks
             frozen to the mouse.
  Shift+F12  the screen recorder, which starts writing mp4s to disk.

None of that is something a shipped app wants a user to find by accident,
and there was no flag, env var or property to stop it.

So: one gate, platform/src/devtools.rs. `--devtools`, or
MAKEPAD_DEVTOOLS=1, and --remote implies it since the /snap + /click loop
drives the tweaker. An explicit MAKEPAD_DEVTOOLS=0 wins over all of it,
which also keeps the off path testable under --remote.

Only the hotkeys are gated, not the tools. Cx::sploded_toggle,
set_tweak_on and ScreenCap::toggle are untouched and still public, so an
app that wants any of this puts it on a key of its own choosing -- that's
the app deciding, rather than a key nobody knew was bound.

Gating F10 and F12 is enough to reach all of it: everything else these
two claim sits behind `sploded.active` / `tweak_is_on()`, and with the
hotkeys gated the only remaining ways in are the /tweak routes (already
--remote, which implies devtools) and an app's own call.

* text_input: drop the Ctrl+Enter submit clause again

`|| mods.control` made Ctrl+Enter submit a multiline input. On
Linux/Windows that's already what is_primary() means, so it changed
nothing; on macOS it turned Ctrl+Enter from "insert a newline" into
"send", which is a surprise in the middle of a chat app's composer.

The comment right above it already described the old behavior, so this
puts the code back in line with it.
2026-09-02 23:41:39 +02:00
7 changed files with 147 additions and 18 deletions

92
platform/src/devtools.rs Normal file
View file

@ -0,0 +1,92 @@
//! 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,6 +78,7 @@ pub mod display_context;
#[macro_use]
mod app_main;
pub mod remote;
pub mod devtools;
pub mod pixel_probe;
pub mod screen_capture;
pub mod audio_output_tap;

View file

@ -46,11 +46,14 @@ mod imp {
static ACTIVE: AtomicBool = AtomicBool::new(false);
/// True when this process was started with `--remote` (any form). Pure
/// argv scan, usable before the bridge itself is up — the platform's
/// focus policy reads it while the first window is being created.
/// True when this process asked for the remote bridge, in any of the forms
/// [`requested_bind`] accepts — including `MAKEPAD_REMOTE`, which a plain
/// argv scan used to miss, so `MAKEPAD_REMOTE=1` started the bridge while
/// 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 {
std::env::args().any(|a| a == "--remote" || a.starts_with("--remote="))
requested_bind().is_some()
}
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
static LIVE_CONNS: AtomicUsize = AtomicUsize::new(0);
@ -458,9 +461,14 @@ mod imp {
/// "the user dismissed this" apart from "the app crashed", and remember it
/// so requests aimed at that window get the real reason.
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:?})");
println!("{line}");
let _ = std::io::stdout().flush();
if is_active() {
println!("{line}");
let _ = std::io::stdout().flush();
}
push_log_line(line);
if let Ok(mut closed) = closed_windows().lock() {
if !closed.iter().any(|(id, _)| *id == window_id) {
@ -473,8 +481,10 @@ mod imp {
/// away. Not a crash.
pub fn note_user_closed_last_window() {
let line = "[makepad-remote] app exit: user closed the last window".to_string();
println!("{line}");
let _ = std::io::stdout().flush();
if is_active() {
println!("{line}");
let _ = std::io::stdout().flush();
}
push_log_line(line);
}
@ -2128,6 +2138,10 @@ mod imp {
use crate::cx::Cx;
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 {
false
}

View file

@ -11,7 +11,8 @@
//! scrollbar thumb by dragging is therefore deliberately NOT possible while
//! exploded — a drag is the orbit — and that is the coexistence rule.
//!
//! F10 tilts the window into an isometric stack that renders **the component
//! F10 — once the dev overlays are switched on ([`crate::devtools`]) — tilts
//! the window into an isometric stack that renders **the component
//! nesting structure**: one plane per nesting level, siblings sharing a plane,
//! 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
@ -727,7 +728,12 @@ impl Cx {
}
match event {
Event::KeyDown(e) => {
if e.key_code == KeyCode::F10 {
// F10 is only ours when the app opted into the dev overlays
// (`--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 {
return true;
}

View file

@ -1,5 +1,9 @@
//! 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
//! 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,
@ -43,6 +47,7 @@ use crate::makepad_draw::audio::AudioBuffer;
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::devtools;
use makepad_platform::screen_capture::{
add_screen_capture, remove_screen_capture, ScreenCaptureOptions,
};
@ -321,7 +326,12 @@ impl ScreenCap {
impl Widget for ScreenCap {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
if let Event::KeyDown(ke) = event {
if ke.key_code == self.hotkey
// The hotkey only exists once the dev overlays are switched on
// (`--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.is_repeat
{

View file

@ -2653,12 +2653,8 @@ impl Widget for TextInput {
// In multiline mode, other modifier combos (Alt+Enter, or Ctrl+Enter
// on macOS) insert a newline below when not read-only.
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
|| mods.is_primary()
|| mods.control
|| (has_physical_keyboard && self.submit_on_enter && !mods.any());
if should_submit {
cx.hide_text_ime();

View file

@ -1,7 +1,10 @@
//! The TWEAKER — the design-feedback overlay every `--remote` app grows.
//!
//! Hardcoded into `Window` (like the caption bar: zero app wiring), inert
//! unless the remote bridge is live, zero cost while off. Turned on (F12 or
//! unless the remote bridge is live, zero cost while off. The F12 key needs
//! 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
//! AI watches the same session through the bridge:
//!
@ -29,6 +32,7 @@
use crate::{
check_box::{CheckBox, CheckBoxAction},
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},
file_tree::{FileTree, FileTreeAction},
label::Label,
@ -616,13 +620,19 @@ pub fn window_intercept(
// F12 toggles the mode, bridge or no bridge: the design surface is
// in-process and owes the remote nothing. Only the HTTP endpoints and
// the AI vibecode loop need --remote; without it they simply are not
// there, and the panel still is.
// there, and the panel still is. It does need the dev overlays to be
// 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
// (widgets/src/screen_cap.rs), and it must not drag the design surface
// into every recording.
if let Event::KeyDown(key_event) = event {
if key_event.key_code == KeyCode::F12 && !key_event.modifiers.shift {
if key_event.key_code == KeyCode::F12
&& !key_event.modifiers.shift
&& devtools::enabled()
{
let flip = {
let mut s = session().lock().unwrap();
if s.toggle_event_id != cx.event_id() {