Compare commits

..

No commits in common. "4b25a1bf1ed3a1dbb1a7cfea1a030ab8c290fc5a" and "493d23a7630f487d29912dd73f2cbb5b639b74ca" have entirely different histories.

37 changed files with 395 additions and 3395 deletions

View file

@ -140,21 +140,6 @@ fn derive_script_impl_inner(
.iter()
.any(|a| a.name == "live" || a.name == "apply_default")
{
// A field whose canonical mutation path is an imperative
// setter (`#[visible]` → `set_visible`, `#[imperative]` for
// the rest) shares its storage with the DSL value, so a
// re-walk that carries no authored change must leave it alone.
// Otherwise `script_mod` re-runs that exist only to re-bake
// heap primitives — every safe-area inset change, i.e. every
// Android system-bar hide and every rotation — silently put
// the DSL default back over the runtime state.
let imperative = field
.attrs
.iter()
.any(|a| a.name == "imperative" || a.name == "visible");
if imperative {
tb.add("if !apply.preserves_runtime_state() {");
}
tb.add("{ let mut __field_value = vm.bx.heap.value_for_apply(value, id!(")
.ident(&field.name)
.add(").into(), apply);");
@ -174,9 +159,6 @@ fn derive_script_impl_inner(
.add(",vm, apply, scope, v);");
tb.add("}");
tb.add("}");
if imperative {
tb.add("}");
}
}
if field
.attrs

View file

@ -36,7 +36,6 @@ pub fn script_err_gen(input: TokenStream) -> TokenStream {
source,
new,
live,
imperative,
rust,
pick,
splat,

View file

@ -132,21 +132,6 @@ pub enum Apply {
/// is the new source of truth and template values should override
/// any prior runtime state.
Reload,
/// `script_mod` was re-run and the fresh template re-walked, but the DSL
/// source did NOT change — the re-run exists only to re-bake heap
/// primitives that are evaluated at `script_mod` time and are therefore
/// unreachable to `ScriptReapply` (canonical case:
/// `mod.widgets.SAFE_INSET_PAD_*` after a safe-area inset change, which
/// `cx.request_live_edit()` triggers on every Android system-bar hide and
/// every rotation).
///
/// Structurally this is a reload — children lists are rebuilt and the
/// `#[source]` object is re-bound, because the template really is a new
/// object. Semantically it is a `ScriptReapply` — nothing the developer
/// wrote changed, so imperative runtime state must survive. Routing it
/// through `Reload` is what used to wipe every `set_text`, every
/// `set_visible`, and every animator state on each inset change.
Rebake,
/// Heap-mutation broadcast triggered by `cx.request_script_reapply()`
/// (e.g. preference change, safe-area inset change). The template has
/// NOT changed — the same cached `app_value` is being re-walked so
@ -165,7 +150,6 @@ impl Apply {
match self {
Self::New => true,
Self::Reload => true,
Self::Rebake => true,
Self::ScriptReapply => true,
Self::Eval => true,
_ => false,
@ -177,13 +161,11 @@ impl Apply {
/// creates temporary objects that would become dangling after GC.
/// Excludes ScriptReapply because the template hasn't changed —
/// the same source object is being re-walked, so re-binding it is
/// unnecessary work. Includes `Rebake`: `script_mod` re-ran, so the
/// source object really is new even though the DSL text is unchanged.
/// unnecessary work.
pub fn is_template_apply(&self) -> bool {
match self {
Self::New => true,
Self::Reload => true,
Self::Rebake => true,
_ => false,
}
}
@ -210,15 +192,13 @@ impl Apply {
pub fn is_reload(&self) -> bool {
match self {
Self::Reload => true,
Self::Rebake => true,
Self::ScriptReapply => true,
_ => false,
}
}
/// True only for `Apply::Reload` — a LiveEdit-driven hot-reload where
/// the DSL itself changed. Excludes `Apply::ScriptReapply` and
/// `Apply::Rebake` (a `script_mod` re-run with unchanged source). Use this
/// the DSL itself changed. Excludes `Apply::ScriptReapply`. Use this
/// (rather than `is_reload`) when behavior should fire only when the
/// template source has actually changed (e.g. re-running script_mod
/// scaffolding, invalidating template-derived caches).
@ -230,11 +210,10 @@ impl Apply {
}
/// True only for `Apply::ScriptReapply` — a `request_script_reapply`-driven
/// re-walk where the template has NOT changed.
///
/// Prefer `preserves_runtime_state()` when the question is "may I clobber
/// the runtime value here?"; this predicate exists for the narrower cases
/// that care about the specific trigger.
/// re-walk where the template has NOT changed. Field impls whose value
/// should not be clobbered by template defaults on this kind of re-walk
/// should early-return when this is true (canonical example:
/// `ArcStringMut::script_apply`).
pub fn is_script_reapply(&self) -> bool {
match self {
Self::ScriptReapply => true,
@ -242,40 +221,6 @@ impl Apply {
}
}
/// True for the walks that follow a `script_mod` re-run (`Reload`,
/// `Rebake`). The re-run builds a fresh object heap, so any script object
/// reference a widget cached during an earlier walk is now dangling —
/// `Animator`'s cached state objects are the canonical example. Code
/// holding such references must re-resolve them from the incoming value
/// rather than reuse what it stored.
///
/// `ScriptReapply` is excluded: it re-walks the same cached `app_value`,
/// so cached references stay alive.
pub fn follows_script_rerun(&self) -> bool {
match self {
Self::Reload => true,
Self::Rebake => true,
_ => false,
}
}
/// True for the re-apply walks where nothing the developer wrote changed,
/// so template values must NOT overwrite state that was set through an
/// imperative setter (`Label::set_text`, `set_visible`, animator state).
/// Field impls whose canonical mutation path is such a setter should
/// early-return when this is true (canonical example:
/// `ArcStringMut::script_apply`).
///
/// Excludes `Apply::Reload`, where the DSL genuinely changed and the new
/// template is meant to win.
pub fn preserves_runtime_state(&self) -> bool {
match self {
Self::ScriptReapply => true,
Self::Rebake => true,
_ => false,
}
}
pub fn is_animate(&self) -> bool {
match self {
Self::Animate => true,

View file

@ -44,12 +44,6 @@ impl<'a> ScriptVm<'a> {
return;
};
self.bx.threads.cur().slot_base = call.prev_slot_base;
// A root frame's last scope holds everything the body defined after
// a shadowing let; keep it for host->script lookups.
if call.return_ip.is_none() {
let end = self.bx.threads.cur_ref().scopes.last().copied();
self.bx.threads.cur().root_end_scope = end.map(|s| self.bx.heap.new_object_ref(s));
}
self.bx
.threads
.cur()

View file

@ -340,14 +340,13 @@ script_primitive!(
) {
// Same rationale as `ArcStringMut::script_apply`: text-bearing fields
// (`TextInput.text`, `TextInput.empty_text`, etc.) are canonically
// mutated through imperative setters at runtime. A re-walk that
// carries no authored change (`cx.request_script_reapply()` preference
// broadcast, or the `script_mod` re-run a safe-area inset change
// forces) would otherwise clobber that runtime value with the stale
// DSL literal. Strings are not part of the shared-heap-object
// propagation pipeline (which uses `Size`/numerics), so bailing here
// is safe.
if apply.preserves_runtime_state() {
// mutated through imperative setters at runtime. A ScriptReapply
// walk fired by `cx.request_script_reapply()` (preference broadcast,
// safe-area inset change) would otherwise clobber that runtime value
// with the stale DSL literal. Strings are not part of the shared-
// heap-object propagation pipeline (which uses `Size`/numerics), so
// bailing here is safe.
if apply.is_script_reapply() {
return;
}
self.clear();

View file

@ -111,9 +111,6 @@ pub struct ScriptThread {
/// Base of the CURRENT slot frame (see CallFrame::prev_slot_base).
pub(crate) slot_base: usize,
pub(crate) instruction_limit_remaining: Option<usize>,
/// The innermost scope when the root frame returned, kept alive so
/// the body can hand it to host->script calls.
pub(crate) root_end_scope: Option<ScriptObjectRef>,
pub trap: ScriptTrapInner,
//pub(crate) last_err: ScriptValue,
pub(crate) json_parser: JsonParserThread,
@ -125,7 +122,6 @@ impl ScriptThread {
Self {
thread_id,
is_paused: false,
root_end_scope: None,
//last_err: NIL,
// pre-reserve the hot stacks so steady-state execution never
// pays Vec growth in the interpreter loop

View file

@ -39,9 +39,7 @@ pub trait ScriptHook {
// Widgets that need to differentiate can branch on
// `apply.is_live_edit_reload()` or `apply.is_script_reapply()`
// inside the hook.
Apply::Reload | Apply::Rebake | Apply::ScriptReapply => {
self.on_before_reload_scoped(vm, scope)
}
Apply::Reload | Apply::ScriptReapply => self.on_before_reload_scoped(vm, scope),
_ => (),
}
}
@ -64,9 +62,7 @@ pub trait ScriptHook {
) {
match apply {
Apply::New => self.on_after_new_scoped(vm, scope),
Apply::Reload | Apply::Rebake | Apply::ScriptReapply => {
self.on_after_reload_scoped(vm, scope)
}
Apply::Reload | Apply::ScriptReapply => self.on_after_reload_scoped(vm, scope),
_ => (),
}
self.on_alive()

View file

@ -64,9 +64,6 @@ pub struct ScriptBody {
pub parser: ScriptParser,
pub scope: ScriptObjectRef,
pub me: ScriptObjectRef,
/// The scope the body ended in, once it has run: `let`/`fn` that
/// shadow a name open child scopes below `scope`.
pub end_scope: Option<ScriptObjectRef>,
pub checkpoint: Option<ParserCheckpoint>,
pub source_len: usize,
}
@ -975,11 +972,7 @@ impl<'a> ScriptVm<'a> {
self.bx.threads.cur().trap.ip.index = 0;
// the main interpreter loop
let value = self.run_core();
if let Some(end) = self.bx.threads.cur().root_end_scope.take() {
self.bx.code.bodies.borrow_mut()[body_id as usize].end_scope = Some(end);
}
value
self.run_core()
}
/// Checks if the value has an apply transform and calls it, returning the transformed value.
@ -1312,7 +1305,6 @@ impl<'a> ScriptVm<'a> {
parser: ScriptParser::default(),
scope,
me,
end_scope: None,
checkpoint: None,
source_len: 0,
};

View file

@ -1,53 +0,0 @@
//! Host->script hook lookups against a Splash-shaped body: the script is
//! wrapped in an auto-closed object literal, and a `let`/`fn` that shadows
//! an existing name opens a child scope the module scope cannot see into.
//! The body remembers the scope it ended in for exactly that.
use makepad_script::*;
fn test_vm() -> ScriptVm<'static> {
let host = Box::leak(Box::new(0i32));
let std = Box::leak(Box::new(0i32));
ScriptVm { host, std, bx: Box::new(ScriptVmBase::new()) }
}
fn scopes(vm: &mut ScriptVm, file: &str) -> (ScriptObject, Option<ScriptObject>) {
let bodies = vm.bx.code.bodies.borrow();
bodies.iter().find_map(|body| match &body.source {
ScriptSource::Mod(m) if m.file == file => {
Some((body.scope.as_object(), body.end_scope.as_ref().map(|s| s.as_object())))
}
_ => None,
}).expect("body")
}
#[test]
fn hooks_resolve_in_the_scope_the_body_ended_in() {
let mut vm = test_vm();
vm.bx.captured_errors = Some(Vec::new());
// `fs` is defined twice: the second `let` shadows and opens a child scope.
let code = "let fs = 1\n{height: 1, let fs = 2\nfn before(){ 7 }\nfn on_x(){ 42 }\n{}\n";
vm.with_instruction_limit(500_000, |vm| {
vm.eval(ScriptMod {
cargo_manifest_path: String::new(),
module_path: String::new(),
file: "hook_scope".to_string(),
line: 0, column: 0,
code: code.to_string(),
values: vec![],
})
});
let errors = vm.take_errors();
assert!(errors.is_empty(), "eval errored: {errors:?}");
let (module, end) = scopes(&mut vm, "hook_scope");
let end = end.expect("the body recorded the scope it ended in");
let from_module = vm.bx.heap.scope_value(module, id!(on_x), NoTrap);
assert!(from_module.is_nil() || from_module.is_err(), "the module scope should not see past the shadowing let");
let on_x = vm.bx.heap.scope_value(end, id!(on_x), NoTrap);
assert!(!on_x.is_nil() && !on_x.is_err(), "on_x not found in the end scope");
let result = vm.with_instruction_limit(500_000, |vm| vm.call(on_x, &[]));
assert_eq!(format!("{result:?}"), "42", "on_x returned {result:?}");
let before = vm.bx.heap.scope_value(end, id!(before), NoTrap);
assert!(!before.is_nil() && !before.is_err(), "before not found in the end scope");
}

View file

@ -287,9 +287,6 @@ macro_rules! _app_main_event_closure {
if let Event::LiveEdit = event {
let mut app_ref = app.borrow_mut();
if let Some(app) = app_ref.as_mut() {
// `Reload` when the DSL changed on disk, `Rebake` when
// `script_mod` only re-ran to pick up new heap primitives.
let live_edit_apply = cx.live_edit_apply();
cx.with_vm(|vm| {
let value = vm.with_reload(|vm| <$app as AppMain>::script_mod(vm));
if let Some(obj) = value.as_object() {
@ -298,7 +295,7 @@ macro_rules! _app_main_event_closure {
<$app as $crate::ScriptApply>::script_apply(
app,
vm,
&live_edit_apply,
&$crate::Apply::Reload,
&mut $crate::Scope::empty(),
value,
);

View file

@ -92,11 +92,11 @@ impl ScriptApply for ArcStringMut {
) {
// Same rationale as `String::script_apply` (see `prims.rs`):
// text-bearing fields are canonically mutated by imperative setters
// (`Label::set_text`, `Button::set_text`, etc.), so a re-walk that
// carries no authored change should not clobber the runtime value with
// the stale DSL literal. Only `Apply::Reload` gets to win, because
// there the DSL itself changed.
if apply.preserves_runtime_state() {
// (`Label::set_text`, `Button::set_text`, etc.), so a ScriptReapply
// walk should not clobber the runtime value with the stale DSL
// literal. `Apply::Reload` (LiveEdit) still applies — DSL just
// changed, so the new template wins.
if apply.is_script_reapply() {
return;
}
// Convert to owned String using the heap's cast method

View file

@ -145,25 +145,16 @@ pub struct Cx {
pub pending_script_reapply: bool,
/// When true, the next event-loop iteration will fire `Event::LiveEdit`,
/// which re-runs `script_mod` and re-applies with `Apply::Rebake`. Use
/// which re-runs `script_mod` and re-applies with `Apply::Reload`. Use
/// this when a primitive heap value (e.g. `mod.widgets.SAFE_INSET_PAD_TOP`)
/// has changed and needs to be re-baked into widget definitions that
/// reference it via expressions like `top: (mod.widgets.SAFE_INSET_PAD_TOP)`
/// — those expressions are only re-evaluated when `script_mod` re-runs.
/// The re-run is still a full-tree walk, so prefer
/// `pending_script_reapply` whenever the change can be modeled as a
/// shared-heap-object mutation instead.
/// `Apply::Reload` clobbers runtime widget state (animator values, etc.),
/// so prefer `pending_script_reapply` whenever the change can be modeled
/// as a shared-heap-object mutation instead.
pub pending_live_edit_request: bool,
/// Which `Apply` variant the pending `Event::LiveEdit` should re-apply
/// the freshly re-run `script_mod` value with. A file-change hot reload
/// means the DSL actually changed, so the new template wins
/// (`Apply::Reload`). A `request_live_edit()` re-bake did not change the
/// DSL, so imperative runtime state must survive (`Apply::Rebake`) —
/// otherwise every safe-area inset change wipes each `set_text`,
/// `set_visible` and animator state in the tree.
pub(crate) live_edit_apply: Apply,
/// `WindowGeomChange` events queued up during an event dispatch.
pub(crate) pending_window_geom_changes: Vec<WindowGeomChangeEvent>,
pub(crate) clear_hover_queued: bool,
@ -534,7 +525,6 @@ impl Cx {
display_context: Default::default(),
pending_script_reapply: false,
pending_live_edit_request: false,
live_edit_apply: Apply::Reload,
pending_window_geom_changes: Default::default(),
clear_hover_queued: false,

View file

@ -576,26 +576,19 @@ impl Cx {
/// Requests a deferred `Event::LiveEdit` on the next event-loop iteration.
/// The handler re-runs `script_mod` (re-evaluating any expressions that
/// reference primitive heap values like `mod.widgets.SAFE_INSET_PAD_TOP`)
/// and then re-applies the widget tree with `Apply::Rebake`, which
/// preserves imperative runtime state since the DSL itself is unchanged.
/// and then re-applies the widget tree with `Apply::Reload`.
///
/// Use this only when a primitive heap value has changed and that value
/// is consumed by `script_mod!` block expressions — those expressions are
/// not re-evaluated by `Apply::ScriptReapply`. The re-run is still a full
/// tree walk, so prefer `request_script_reapply` when the change can be
/// not re-evaluated by `Apply::ScriptReapply`. `Apply::Reload` walks
/// clobber runtime widget state (animator values, dynamic instance
/// buffers, user-typed text in widgets that don't early-return on
/// LiveEdit), so prefer `request_script_reapply` when the change can be
/// modeled as a shared-heap-object mutation instead.
pub fn request_live_edit(&mut self) {
self.pending_live_edit_request = true;
}
/// The `Apply` variant the currently dispatching `Event::LiveEdit` should
/// be re-applied with — `Reload` for a file-change hot reload, `Rebake`
/// for a `request_live_edit()` re-bake. `app_main!` reads this; app code
/// has no reason to.
pub fn live_edit_apply(&self) -> crate::makepad_script::Apply {
self.live_edit_apply.clone()
}
/// Remap an absolute coordinate from the OS-reported logical-point space
/// into the layout's logical-point space when a `dpi_override` is active
/// on the given window. No-op if no override is set or `os_dpi_factor`

View file

@ -487,12 +487,6 @@ impl CxFingers {
self.captures.iter().find(|v| v.area == area).is_some()
}
/// Whether `digit_id` is held by an area other than `area`, so a
/// capture-overload hit can tell a press a child already owns.
pub fn is_digit_captured_elsewhere(&self, digit_id: DigitId, area: Area) -> bool {
self.captures.iter().any(|v| v.digit_id == digit_id && v.area != area)
}
/// The area that captured the touch with the given uid, if any.
/// Lets a raw `Event::LongPress` handler check which widget owns the press.
pub fn touch_capture_area(&self, uid: u64) -> Option<Area> {

View file

@ -245,8 +245,8 @@ pub use {
web_socket::{WebSocket, WebSocketMessage},
window::{
CxWindowPool, MacosWindowChrome, MacosWindowConfig, MacosWindowKind, MacosWindowLevel,
ScriptWindowHandle, WaylandDecorationPreference, WindowBackdrop, WindowHandle,
WindowIcon, WindowIconBuffer, WindowId, WindowVisuals,
ScriptWindowHandle, WindowBackdrop, WindowHandle, WindowIcon, WindowIconBuffer,
WindowId, WindowVisuals,
},
xr_tsdf::{
ChunkKey, SparseTsdGridReadSnapshot, SparseTsdReadChunk, TsdfPublishedSnapshot,

View file

@ -890,7 +890,6 @@ impl Cx {
LiveEditTrigger::FileChange => {
self.draw_shaders.reset_for_live_reload();
self.pending_script_reapply = false;
self.live_edit_apply = crate::makepad_script::Apply::Reload;
self.call_event_handler(&Event::LiveEdit);
self.redraw_all();
if self.pending_script_reapply {
@ -905,10 +904,6 @@ impl Cx {
// app-level handler that re-broadcasts sets a fresh flag
// that lands on the next tick.
self.pending_script_reapply = false;
// The DSL did not change, so re-apply with `Rebake`: the
// re-run only exists to pick up new `SAFE_INSET_PAD_*`
// values, and imperative runtime state must survive it.
self.live_edit_apply = crate::makepad_script::Apply::Rebake;
self.call_event_handler(&Event::LiveEdit);
self.redraw_all();
}

View file

@ -281,31 +281,6 @@ impl OpenglCx {
}
}
/// Makes this context current on `egl_surface` for both drawing and reading.
///
/// Wayland must bind the target surface before resizing its `wl_egl_window`:
/// some EGL implementations defer a resize of a non-current surface until
/// the next swap, which would render one frame with mismatched buffer geometry.
pub(crate) fn make_current_with_surface(&self, egl_surface: egl_sys::EGLSurface) -> bool {
unsafe {
let ok = (self.libegl.eglMakeCurrent.unwrap())(
self.egl_display,
egl_surface,
egl_surface,
self.egl_context,
);
if ok == 0 {
// `eglGetError` is called outside the latch: it clears EGL's per-thread
// error, and skipping it would leak a stale code into the next report.
let egl_error = (self.libegl.eglGetError.unwrap())();
self.report_egl_error("eglMakeCurrent(window surface)", egl_error);
return false;
}
self.make_current_error_logged.set(false);
true
}
}
/// Logs an `eglMakeCurrent` failure once per outage, naming a lost context explicitly
/// so a report of "the window went black" arrives with its cause attached.
fn report_egl_error(&self, what: &str, egl_error: egl_sys::EGLint) {
@ -355,9 +330,20 @@ impl Cx {
unsafe {
let gl = self.os.gl();
let opengl_cx = self.os.opengl_cx.as_ref().unwrap();
if !opengl_cx.make_current_with_surface(egl_surface) {
let make_current_ok = (opengl_cx.libegl.eglMakeCurrent.unwrap())(
opengl_cx.egl_display,
egl_surface,
egl_surface,
opengl_cx.egl_context,
);
if make_current_ok == 0 {
// `eglGetError` is called outside the latch: it clears EGL's per-thread
// error, and skipping it would leak a stale code into the next report.
let egl_error = (opengl_cx.libegl.eglGetError.unwrap())();
opengl_cx.report_egl_error("eglMakeCurrent", egl_error);
return false;
}
opengl_cx.make_current_error_logged.set(false);
// Apply the configured swap interval (vsync) on the now-current window surface.
// Re-applied per frame because it is surface-scoped and surfaces are recreated
// on resize; the call is cheap and idempotent.

View file

@ -37,8 +37,7 @@ use crate::{
gpu_info::GpuPerformance,
texture::TextureFormat,
Area, Cx, CxDrawPassParent, CxOsOp, CxWindowPool, Event, KeyModifiers, MouseButton,
MouseMoveEvent, MouseUpEvent, SignalToUI, WaylandDecorationPreference, WindowClosedEvent,
WindowGeomChangeEvent,
MouseMoveEvent, MouseUpEvent, SignalToUI, WindowClosedEvent, WindowGeomChangeEvent,
};
use wayland_client::protocol::{wl_keyboard, wl_pointer};
use wayland_client::{Connection, Proxy};
@ -51,41 +50,6 @@ fn log_linux_backdrop_unsupported_once() {
});
}
fn parse_decoration_preference(value: &str) -> Option<WaylandDecorationPreference> {
match value {
"server" | "server-side" => Some(WaylandDecorationPreference::ServerSide),
"client" | "client-side" => Some(WaylandDecorationPreference::ClientSide),
_ => None,
}
}
fn decoration_preference_override() -> Option<WaylandDecorationPreference> {
std::env::args_os()
.find_map(|arg| {
arg.to_str()
.and_then(|arg| arg.strip_prefix("--wayland-decoration="))
.and_then(parse_decoration_preference)
})
.or_else(|| {
std::env::var("MAKEPAD_WAYLAND_DECORATION")
.ok()
.as_deref()
.and_then(parse_decoration_preference)
})
}
/// Whether a window's surface may be promised opaque to the compositor.
///
/// This is the same decision the macOS backend makes from the same two fields for its
/// layer's `opaque` flag, and the one the Windows backend makes before enabling
/// composition, so a window that is opaque on one platform is opaque on all of them.
/// `transparent` is the documented opt-in for a see-through window; a backdrop effect
/// needs the surface translucent to sample what is behind it, so it forfeits the
/// promise as well, whether or not this platform implements it yet.
fn window_is_opaque(transparent: bool, backdrop: crate::window::WindowBackdrop) -> bool {
!transparent && backdrop == crate::window::WindowBackdrop::None
}
pub fn wayland_event_loop(cx: Rc<RefCell<Cx>>) {
WaylandCx::event_loop_impl(cx);
}
@ -100,7 +64,6 @@ pub(crate) struct WaylandCx {
/// callbacks the compositor withholds) from wedging the event loop. Disabled by
/// `MAKEPAD_NO_VSYNC` for uncapped benchmarking.
frame_pacing: bool,
decoration_preference_override: Option<WaylandDecorationPreference>,
}
impl WaylandCx {
@ -116,7 +79,6 @@ impl WaylandCx {
cx: cx.clone(),
qhandle: None,
frame_pacing: std::env::var_os("MAKEPAD_NO_VSYNC").is_none(),
decoration_preference_override: decoration_preference_override(),
}));
let conn = Connection::connect_to_env().unwrap();
let display = conn.display();
@ -225,31 +187,39 @@ impl WaylandCx {
// do this here because mac
let mut cx = self.cx.borrow_mut();
let window_index = state
.windows
.iter()
.position(|window| window.window_id == re.window_id);
// Wayland's native surface state does not know the geometry of
// Makepad-drawn buttons. Populate it after DPI conversion below.
re.new_geom.window_chrome_buttons = Rect::default();
if let Some(window_index) = window_index {
// compare in native units, before new_geom is converted below
let window = &mut state.windows[window_index];
let uses_csd = window.uses_client_side_decorations;
let is_fullscreen = window.is_fullscreen;
let old_chrome_buttons =
cx.windows[re.window_id].window_geom.window_chrome_buttons;
let decoration_changed = {
let cx_window = &cx.windows[re.window_id];
cx_window.uses_client_side_decorations != uses_csd
|| cx_window.wayland_is_fullscreen != is_fullscreen
// When drawing our own window chrome (no server-side decorations),
// populate the chrome buttons bounding box: three buttons right-aligned
// at the top of the caption bar, matching the Makepad widget layout.
if matches!(
cx.os_type(),
OsType::LinuxWindow(LinuxWindowParams {
custom_window_chrome: true,
..
})
) {
const BUTTONS_W: f64 = 46.0 * 3.0;
const BUTTONS_H: f64 = 29.0;
let w = re.new_geom.inner_size.x;
re.new_geom.window_chrome_buttons = Rect {
pos: Vec2d {
x: w - BUTTONS_W,
y: 0.0,
},
size: Vec2d {
x: BUTTONS_W,
y: BUTTONS_H,
},
};
let geom_changed = decoration_changed
|| window.csd_shadow_needs_update()
|| re.old_geom.inner_size != re.new_geom.inner_size
|| re.old_geom.dpi_factor != re.new_geom.dpi_factor
|| re.old_geom.is_fullscreen != re.new_geom.is_fullscreen;
}
if let Some(window) = state
.windows
.iter_mut()
.find(|w| w.window_id == re.window_id)
{
// compare in native units, before new_geom is converted below
let geom_changed = re.old_geom.inner_size != re.new_geom.inner_size
|| re.old_geom.dpi_factor != re.new_geom.dpi_factor;
// Keep the wayland geom native (buffer/viewport size + the next resize's dpi come
// from it). Store the zoomed geom here and the next resize reads its dpi back as
@ -258,19 +228,9 @@ impl WaylandCx {
window.window_geom = re.new_geom.clone();
{
let cx_window = &mut cx.windows[re.window_id];
cx_window.uses_client_side_decorations = uses_csd;
cx_window.wayland_is_fullscreen = is_fullscreen;
cx_window.os_dpi_factor = Some(re.new_geom.dpi_factor);
re.new_geom = cx_window.native_window_geom_to_layout(re.new_geom);
}
if uses_csd && !is_fullscreen {
const BUTTONS_SIZE: Vec2d = Vec2d { x: 138.0, y: 29.0 };
re.new_geom.window_chrome_buttons = Rect {
pos: dvec2(re.new_geom.inner_size.x - BUTTONS_SIZE.x, 0.0),
size: BUTTONS_SIZE,
};
}
re.old_geom.window_chrome_buttons = old_chrome_buttons;
cx.windows[re.window_id].window_geom = re.new_geom.clone();
// redraw when the size or scale changed
if geom_changed {
@ -587,17 +547,8 @@ impl WaylandCx {
}
cx.call_event_handler(&Event::WindowClosed(WindowClosedEvent { window_id }));
cx.windows[window_id].is_created = false;
// The pointer may have been over the window's shadow gutter rather than the
// window, which leaves no `pointer_window` to match on.
if state.pointer_window == Some(window_id)
|| state
.pointer_shadow
.is_some_and(|(shadow_window, _)| shadow_window == window_id)
{
if state.pointer_window == Some(window_id) {
state.pointer_window = None;
state.pointer_shadow = None;
state.pointer_enter_serial = None;
state.last_resize_edge = None;
}
if state.keyboard_window == Some(window_id) {
state.keyboard_window = None;
@ -625,17 +576,8 @@ impl WaylandCx {
let mut cx = self.cx.borrow_mut();
cx.call_event_handler(&Event::WindowClosed(event));
cx.windows[window_id].is_created = false;
// The pointer may have been over the window's shadow gutter rather than the
// window, which leaves no `pointer_window` to match on.
if state.pointer_window == Some(window_id)
|| state
.pointer_shadow
.is_some_and(|(shadow_window, _)| shadow_window == window_id)
{
if state.pointer_window == Some(window_id) {
state.pointer_window = None;
state.pointer_shadow = None;
state.pointer_enter_serial = None;
state.last_resize_edge = None;
}
if state.keyboard_window == Some(window_id) {
state.keyboard_window = None;
@ -698,13 +640,9 @@ impl WaylandCx {
} else {
&window.create_app_id
};
let decoration_preference = self
.decoration_preference_override
.unwrap_or(window.wayland_decorations);
let window = WaylandWindow::new(
window_id,
compositor,
state.subcompositor.as_ref(),
wm_base,
state.decoration_manager.as_ref(),
state.scale_manager.as_ref(),
@ -718,7 +656,6 @@ impl WaylandCx {
&window.create_title,
app_id,
window.is_fullscreen,
decoration_preference,
);
if cx.windows[window_id].backdrop != crate::window::WindowBackdrop::None {
log_linux_backdrop_unsupported_once();
@ -731,12 +668,8 @@ impl WaylandCx {
// Seed the geom too: the default `dpi_factor` is 0.0, which would make
// `get_pass_rect()` produce NaN once the flag is on.
let native_geom = window.window_geom.clone();
let uses_client_side_decorations = window.uses_client_side_decorations;
let is_fullscreen = window.is_fullscreen;
state.windows.push(window);
let cx_window = &mut cx.windows[window_id];
cx_window.uses_client_side_decorations = uses_client_side_decorations;
cx_window.wayland_is_fullscreen = is_fullscreen;
cx_window.os_dpi_factor = Some(native_geom.dpi_factor);
let layout_geom = cx_window.native_window_geom_to_layout(native_geom);
cx_window.window_geom = layout_geom;
@ -833,20 +766,25 @@ impl WaylandCx {
}
}
CxOsOp::ResizeWindow(window_id, size) => {
// A Wayland client has no "set my size" request. A self-resize changes the
// next EGL buffer, viewport destination, and explicit xdg window geometry;
// the latter excludes any CSD shadow subsurfaces from placement and snapping.
// A Wayland client has no "set my size" request. Window geometry is by
// default whatever the surface commits -- `xdg_surface.set_window_geometry`:
// "If never set, the value is the full bounds of the surface ... This
// updates dynamically on every commit" -- and this backend never sets it,
// so a self-resize is just the next frame committed at a different extent.
// The paint path derives the EGL extent and the viewport destination from
// `window_geom.inner_size`, so writing it here is the whole operation.
//
// Only a floating toplevel may choose its own size. Under xdg_toplevel's
// `maximized` state the configured window geometry must be obeyed "or the
// xdg_wm_base.invalid_surface_state error is raised", which disconnects the
// client; under `fullscreen` the configured geometry is a maximum. Popups
// take their extent from their positioner and are deliberately not matched
// here.
// client; under `fullscreen` the configured geometry is a maximum. The
// configure handler folds both states into `is_fullscreen`, so that one
// flag gates the operation. Popups take their extent from their positioner
// and are deliberately not matched here.
if let Some(window) =
state.windows.iter_mut().find(|w| w.window_id == window_id)
{
if window.is_maximized || window.is_fullscreen {
if window.window_geom.is_fullscreen {
crate::error!(
"ResizeWindow ignored: a maximized or fullscreen Wayland toplevel \
must keep the size the compositor configured."
@ -880,9 +818,9 @@ impl WaylandCx {
// by a restored position the way it can be on Windows, macOS and X11.
// xdg_toplevel exposes no absolute-positioning request: `move` is
// interactive and serial-gated ("This request must be used in response to
// some sort of user action"). `xdg_popup.reposition` only moves a popup
// relative to its parent using a new positioner; it cannot place a toplevel at
// absolute screen coordinates. This arm is therefore a permanent no-op.
// some sort of user action"), and `reposition` is an xdg_popup request
// requiring xdg_wm_base v3, which `wayland_state.rs` does not bind. This arm
// is correct as a permanent no-op.
CxOsOp::RepositionWindow(_window_id, _size) => {}
CxOsOp::SetWindowTitle(window_id, title) => {
if let Some(window) = state.windows.iter().find(|w| w.window_id == window_id) {
@ -927,15 +865,9 @@ impl WaylandCx {
cx.call_event_handler(&Event::DragEnd);
}
CxOsOp::SetCursor(cursor) => {
state.requested_cursor = cursor;
// Native CSD resize hit-testing owns the cursor at an edge, and in the
// shadow gutter the pointer is outside the window entirely, so the app
// has no say over it there either.
if state.last_resize_edge.is_none() && state.pointer_shadow.is_none() {
if let Some(cursor_shape) = state.cursor_shape.as_ref() {
if let Some(serial) = state.pointer_enter_serial.as_ref() {
cursor_shape.set_shape(*serial, cursor.into());
}
if let Some(cursor_shape) = state.cursor_shape.as_ref() {
if let Some(serial) = state.pointer_serial.as_ref() {
cursor_shape.set_shape(*serial, cursor.into());
}
}
}
@ -1244,26 +1176,13 @@ impl WaylandCx {
continue;
}
let mut presented = false;
let opaque = {
let cx_window = &cx.windows[window_id];
window_is_opaque(cx_window.transparent, cx_window.backdrop)
};
let compositor = state.compositor.clone();
if let Some(window) =
state.windows.iter_mut().find(|w| w.window_id == window_id)
{
if !window.configured {
continue;
}
if !window.prepare_buffer_size(cx.os.opengl_cx.as_ref().unwrap()) {
continue;
}
window.prepare_csd_shadow();
if let (Some(compositor), Some(qhandle)) =
(compositor.as_ref(), self.qhandle.as_ref())
{
window.sync_opaque_region(compositor, qhandle, opaque);
}
window.resize_buffers();
if std::env::var_os("MAKEPAD_WAYLAND_TRACE").is_some() {
crate::log!(
"Wayland paint window={:?} inner=({}, {}) dpi={} pix=({}, {})",
@ -1280,7 +1199,8 @@ impl WaylandCx {
// `wp_viewport.set_destination` raises the `bad_value` protocol
// error, which disconnects the client, on a zero or negative
// extent, and a float-to-int cast turns both a negative and a NaN
// into zero. Floor the destination the same way as the EGL extent.
// into zero. Floor the destination the way `resize_buffers` floors
// the EGL extent.
viewport.set_destination(
window.window_geom.inner_size.x.max(1.0) as i32,
window.window_geom.inner_size.y.max(1.0) as i32,
@ -1310,15 +1230,14 @@ impl WaylandCx {
if !window.configured {
continue;
}
if !window.prepare_buffer_size(cx.os.opengl_cx.as_ref().unwrap()) {
continue;
}
window.resize_buffers();
if let Some(viewport) = window.viewport.as_ref() {
viewport.set_source(-1., -1., -1., -1.);
// `wp_viewport.set_destination` raises the `bad_value` protocol
// error, which disconnects the client, on a zero or negative
// extent, and a float-to-int cast turns both a negative and a NaN
// into zero. Floor the destination the same way as the EGL extent.
// into zero. Floor the destination the way `resize_buffers` floors
// the EGL extent.
viewport.set_destination(
window.window_geom.inner_size.x.max(1.0) as i32,
window.window_geom.inner_size.y.max(1.0) as i32,
@ -1357,33 +1276,3 @@ impl WaylandCx {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_window_that_asked_for_translucency_gives_up_the_opaque_promise() {
use crate::window::WindowBackdrop;
assert!(window_is_opaque(false, WindowBackdrop::None));
// Promising opacity for a window that wanted to be see-through would leave
// whatever is behind it on screen, so both opt-ins must veto it.
assert!(!window_is_opaque(true, WindowBackdrop::None));
assert!(!window_is_opaque(false, WindowBackdrop::Blur));
assert!(!window_is_opaque(false, WindowBackdrop::Acrylic));
assert!(!window_is_opaque(true, WindowBackdrop::Blur));
}
#[test]
fn parses_wayland_decoration_override_values() {
assert_eq!(
parse_decoration_preference("server"),
Some(WaylandDecorationPreference::ServerSide)
);
assert_eq!(
parse_decoration_preference("client-side"),
Some(WaylandDecorationPreference::ClientSide)
);
assert_eq!(parse_decoration_preference("invalid"), None);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,6 @@ use crate::window::MacosWindowKind;
use crate::window::MacosWindowLevel;
use crate::window::ScriptWindowHandle;
use crate::window::WindowBackdrop;
use crate::window::WaylandDecorationPreference;
use crate::*;
pub fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
@ -21,7 +20,6 @@ pub fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
set_script_value_to_pod!(vm, draw.DrawPassUniforms);
set_script_value_to_api!(vm, draw.MouseCursor);
set_script_value_to_api!(vm, draw.WindowBackdrop);
set_script_value_to_api!(vm, draw.WaylandDecorationPreference);
set_script_value_to_api!(vm, draw.MacosWindowKind);
set_script_value_to_api!(vm, draw.MacosWindowChrome);
set_script_value_to_api!(vm, draw.MacosWindowLevel);

View file

@ -50,21 +50,6 @@ pub enum MacosWindowLevel {
StatusBar,
}
/// The decoration mode a Wayland toplevel asks the compositor to use.
///
/// `ServerSide` requests compositor decorations, but the compositor may select
/// client-side mode instead. `ClientSide` explicitly uses Makepad's frame. If
/// xdg-decoration is unavailable, Makepad always uses client-side decorations.
/// `MAKEPAD_WAYLAND_DECORATION=client|server` can override it for all windows
/// in a process. `--wayland-decoration=client|server` is also recognized when
/// the application's own argument parser accepts framework arguments.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Script, ScriptHook, Default)]
pub enum WaylandDecorationPreference {
#[default]
ServerSide,
ClientSide,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Script)]
pub struct MacosWindowConfig {
#[live]
@ -368,9 +353,6 @@ impl WindowHandle {
cxwindow.popup_position = None;
cxwindow.popup_size = None;
cxwindow.popup_grab_keyboard = true;
cxwindow.wayland_decorations = WaylandDecorationPreference::default();
cxwindow.uses_client_side_decorations = false;
cxwindow.wayland_is_fullscreen = false;
cx.platform_ops
.push_back(CxOsOp::CreateWindow(window.window_id()));
window
@ -397,9 +379,6 @@ impl WindowHandle {
cxwindow.popup_position = Some(position);
cxwindow.popup_size = Some(size);
cxwindow.popup_grab_keyboard = true;
cxwindow.wayland_decorations = WaylandDecorationPreference::default();
cxwindow.uses_client_side_decorations = false;
cxwindow.wayland_is_fullscreen = false;
cxwindow.popup_grab_keyboard
};
cx.platform_ops.push_back(CxOsOp::CreatePopupWindow {
@ -441,10 +420,6 @@ pub struct ScriptWindowHandle {
pub backdrop_intensity: f32,
#[live(MacosWindowConfig::default())]
pub macos: MacosWindowConfig,
/// Wayland only. Server-side decorations are requested by default; set
/// this to `ClientSide` to explicitly use Makepad's window frame.
#[live(WaylandDecorationPreference::ServerSide)]
pub wayland_decorations: WaylandDecorationPreference,
/// Optionally override the caption bar height.
/// * If `None` (the default), the caption bar's height is based on a system-calculated height
/// derived from window chrome button geometry, which will make the window chrome buttons
@ -495,9 +470,6 @@ impl ScriptHook for ScriptWindowHandle {
.normalized();
let macos = self.macos.normalized();
cx.windows[window_id].macos = macos;
if !cx.windows[window_id].is_created {
cx.windows[window_id].wayland_decorations = self.wayland_decorations;
}
if cx.windows[window_id].window_visuals() != visuals {
cx.windows[window_id].transparent = visuals.transparent;
cx.windows[window_id].backdrop = visuals.backdrop;
@ -549,19 +521,6 @@ impl WindowHandle {
pub fn configure_macos_window(&mut self, cx: &mut Cx, config: MacosWindowConfig) {
cx.windows[self.window_id()].macos = config.normalized();
}
/// Selects the decoration mode requested when a Wayland window is created.
/// This has no effect after the native window has been created.
pub fn configure_wayland_decorations(
&mut self,
cx: &mut Cx,
preference: WaylandDecorationPreference,
) {
let window = &mut cx.windows[self.window_id()];
if !window.is_created {
window.wayland_decorations = preference;
}
}
pub fn get_inner_size(&self, cx: &Cx) -> Vec2d {
cx.windows[self.window_id()].get_inner_size()
}
@ -604,17 +563,6 @@ impl WindowHandle {
cx.windows[self.window_id()].window_geom.is_fullscreen
}
/// Whether this Wayland window is using Makepad-drawn decorations.
pub fn uses_wayland_client_side_decorations(&self, cx: &Cx) -> bool {
cx.windows[self.window_id()].uses_client_side_decorations
}
/// Whether this Wayland window is in true compositor fullscreen, as
/// distinct from the legacy `is_fullscreen` maximize-or-fullscreen flag.
pub fn is_wayland_fullscreen(&self, cx: &Cx) -> bool {
cx.windows[self.window_id()].wayland_is_fullscreen
}
pub fn xr_is_presenting(&mut self, cx: &mut Cx) -> bool {
cx.windows[self.window_id()].window_geom.xr_is_presenting
}
@ -735,11 +683,6 @@ pub struct CxWindow {
pub backdrop: WindowBackdrop,
pub backdrop_intensity: f32,
pub macos: MacosWindowConfig,
pub wayland_decorations: WaylandDecorationPreference,
/// Effective Wayland decoration mode selected by the compositor.
pub(crate) uses_client_side_decorations: bool,
/// True compositor fullscreen, kept separate so CSD remains visible when maximized.
pub(crate) wayland_is_fullscreen: bool,
}
impl Default for CxWindow {
@ -766,9 +709,6 @@ impl Default for CxWindow {
backdrop: WindowBackdrop::None,
backdrop_intensity: 1.0,
macos: MacosWindowConfig::default(),
wayland_decorations: WaylandDecorationPreference::default(),
uses_client_side_decorations: false,
wayland_is_fullscreen: false,
}
}
}
@ -984,32 +924,6 @@ mod tests {
assert!(!cx_window.transparent);
assert_eq!(cx_window.backdrop, WindowBackdrop::None);
assert_eq!(cx_window.backdrop_intensity, 1.0);
assert_eq!(
cx_window.wayland_decorations,
WaylandDecorationPreference::ServerSide
);
}
#[test]
fn reused_window_slot_resets_wayland_decoration_state() {
let mut cx = test_cx();
let first = WindowHandle::new(&mut cx);
let first_id = first.window_id();
cx.windows[first_id].wayland_decorations = WaylandDecorationPreference::ClientSide;
cx.windows[first_id].uses_client_side_decorations = true;
cx.windows[first_id].wayland_is_fullscreen = true;
drop(first);
let second = WindowHandle::new(&mut cx);
let second_id = second.window_id();
assert_eq!(second_id.0, first_id.0);
assert_ne!(second_id.1, first_id.1);
assert_eq!(
cx.windows[second_id].wayland_decorations,
WaylandDecorationPreference::ServerSide
);
assert!(!cx.windows[second_id].uses_client_side_decorations);
assert!(!cx.windows[second_id].wayland_is_fullscreen);
}
#[test]
@ -1170,7 +1084,6 @@ mod tests {
backdrop: WindowBackdrop::Blur,
backdrop_intensity: 0.5,
macos: MacosWindowConfig::floating_panel(),
wayland_decorations: WaylandDecorationPreference::ClientSide,
caption_bar_height_override: None,
};
@ -1193,10 +1106,6 @@ mod tests {
}
);
assert_eq!(cx_window.macos, MacosWindowConfig::floating_panel());
assert_eq!(
cx_window.wayland_decorations,
WaylandDecorationPreference::ClientSide
);
}
#[test]

View file

@ -236,7 +236,7 @@ pub fn list_profiles()->Result<(), String>{
}
*/
impl PlistValues {
pub(super) fn to_plist_file(&self, os: AppleOs) -> String {
fn to_plist_file(&self, os: AppleOs) -> String {
match os {
AppleOs::Tvos => self.to_tvos_plist_file(),
AppleOs::Ios => self.to_ios_plist_file(),
@ -701,35 +701,10 @@ pub fn build(
apple_target: AppleTarget,
) -> Result<IosBuildResult, String> {
let build_crate = get_build_crate_from_args(args)?;
let cwd = std::env::current_dir().unwrap();
let info_plist = crate::apple::info_plist::load(&cwd, build_crate, apple_target.os())?;
let binary_name =
get_package_binary_name(build_crate).unwrap_or_else(|| build_crate.to_string());
// Capitalize the first letter for the user-visible name (CFBundleDisplayName /
// CFBundleName) so the iOS home-screen icon doesn't show a lowercased crate name,
// while keeping the bundle identifier lowercase so existing provisioning profiles
// still match.
let display_name = {
let mut chars = product.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
};
let plist = PlistValues {
identifier: format!("{org}.{product}").to_string(),
display_name: display_name.clone(),
name: display_name,
executable: binary_name.clone(),
version: "1.0.0".to_string(),
};
let generated_plist = plist.to_plist_file(apple_target.os());
let plist_contents = match info_plist {
Some(overrides) => overrides.merge(&generated_plist)?,
None => generated_plist,
};
let cwd = std::env::current_dir().unwrap();
let target_dir = cargo_target_dir(&cwd);
let target_dir_str = target_dir.to_string_lossy().to_string();
let target_dir_arg = format!("--target-dir={target_dir_str}");
@ -770,6 +745,25 @@ pub fn build(
}
shell_env(&rust_env, &cwd, "rustup", &args_out)?;
// alright lets make the .app file with manifest
// Capitalize the first letter for the user-visible name (CFBundleDisplayName /
// CFBundleName) so the iOS home-screen icon doesn't show a lowercased crate name,
// while keeping the bundle identifier lowercase so existing provisioning profiles
// still match.
let display_name = {
let mut chars = product.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
};
let plist = PlistValues {
identifier: format!("{org}.{product}").to_string(),
display_name: display_name.clone(),
name: display_name,
executable: binary_name.clone(),
version: "1.0.0".to_string(),
};
let profile = get_profile_from_args(args);
let app_dir = target_dir.join(format!(
@ -779,7 +773,7 @@ pub fn build(
mkdir(&app_dir)?;
let plist_file = app_dir.join("Info.plist");
write_text(&plist_file, &plist_contents)?;
write_text(&plist_file, &plist.to_plist_file(apple_target.os()))?;
if matches!(apple_target.os(), AppleOs::Ios) {
match generate_app_icon_xcassets(&app_dir, build_crate) {

View file

@ -1,161 +0,0 @@
use super::AppleOs;
use makepad_micro_serde::{DeJson, DeJsonErr, DeJsonState, JsonValue};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(test)]
mod tests;
pub(super) struct InfoPlist {
path: PathBuf,
xml: String,
}
#[derive(DeJson)]
struct CargoMetadata {
packages: Vec<CargoPackage>,
}
#[derive(DeJson)]
struct CargoPackage {
name: String,
version: String,
id: String,
manifest_path: String,
metadata: JsonValue,
}
/// Load an optional app-owned plist relative to the selected Cargo package,
/// never the invoking workspace directory. iOS and tvOS configure it separately.
pub(super) fn load(cwd: &Path, build_crate: &str, os: AppleOs) -> Result<Option<InfoPlist>, String> {
// Let Cargo parse the manifest and resolve the selected workspace member.
// A file URL from `cargo pkgid` is not a filesystem path (e.g. `%20`).
let output = Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version", "1"])
.current_dir(cwd)
.output()
.map_err(|e| format!("Cannot read Cargo metadata for {}: {e}", cwd.join("Cargo.toml").display()))?;
if !output.status.success() {
return Err(format!("Cannot read Cargo metadata for {}: {}",
cwd.join("Cargo.toml").display(), String::from_utf8_lossy(&output.stderr)));
}
let json = std::str::from_utf8(&output.stdout)
.map_err(|e| format!("Cargo metadata is not UTF-8: {e}"))?;
let metadata = CargoMetadata::deserialize_json_lenient(json)
.map_err(|e| format!("Cannot parse Cargo metadata: {e:?}"))?;
let package = metadata.packages.iter().find(|package| {
package.name == build_crate || package.id == build_crate
|| format!("{}@{}", package.name, package.version) == build_crate
}).ok_or_else(|| format!("Cargo metadata does not contain package {build_crate}"))?;
let manifest = Path::new(&package.manifest_path);
let crate_dir = manifest.parent()
.ok_or_else(|| format!("Cargo returned an invalid manifest path: {}", manifest.display()))?;
let platform = match os {
AppleOs::Ios => "ios",
AppleOs::Tvos => "tvos",
};
let key = format!("package.metadata.makepad.{platform}.info_plist");
let configured = package.metadata.key("makepad")
.and_then(|value| value.key(platform))
.and_then(|value| value.key("info_plist"));
let path = match configured {
None => return Ok(None),
Some(JsonValue::String(path)) if !path.is_empty() => crate_dir.join(path),
_ => return Err(format!("{key} in {} must be a nonempty path string", manifest.display())),
};
let xml = read_dictionary(&path)
.map_err(|e| format!("Cannot read custom Info.plist {}: {e}", path.display()))?;
Ok(Some(InfoPlist { path, xml }))
}
impl InfoPlist {
/// Replace whole top-level values, preserving types and unspecified defaults.
/// Bundle identity and executable stay controlled by the build's CLI inputs:
/// changing them here would disagree with signing and simulator launch paths.
pub(super) fn merge(&self, generated: &str) -> Result<String, String> {
self.merge_inner(generated)
.map_err(|e| format!("Cannot merge custom Info.plist {}: {e}", self.path.display()))
}
fn merge_inner(&self, generated: &str) -> Result<String, String> {
let temporary = TemporaryDirectory::new()?;
let defaults = temporary.0.join("defaults.plist");
let merged = temporary.0.join("merged.plist");
fs::write(&defaults, generated.trim_start())
.map_err(|e| format!("Cannot write generated Info.plist: {e}"))?;
read_dictionary(&defaults)?;
fs::write(&merged, &self.xml)
.map_err(|e| format!("Cannot write temporary Info.plist: {e}"))?;
// PlistBuddy's Merge adds only missing top-level keys. Starting with the
// app's dictionary therefore preserves whole custom values, including
// nested dictionaries and arrays. Capture its duplicate-key notices.
// Fixed filenames in a private directory avoid interpolating app paths
// into PlistBuddy's command language; the source file is never modified.
run(Command::new("/usr/libexec/PlistBuddy")
.current_dir(&temporary.0)
.args(["-c", "Merge defaults.plist", "merged.plist"]))?;
for key in ["CFBundleIdentifier", "CFBundleExecutable"] {
// Compare typed, canonical XML so e.g. a boolean cannot pass as a
// string. Check after merging so absent custom keys need no special
// handling, and all command failures remain errors.
let extract = |path: &Path| run(Command::new("/usr/bin/plutil")
.args(["-extract", key, "xml1", "-o", "-", "--"])
.arg(path));
if extract(&merged)? != extract(&defaults)? {
return Err(format!(
"cannot change {key}; use the Apple build options and Cargo binary target instead",
));
}
}
read_dictionary(&merged)
}
}
/// Apple builds already require macOS and its plist utilities. Let plutil parse
/// both XML and binary input; only inspect its normalized XML to check the root
/// type, without implementing a general XML or binary plist parser.
fn read_dictionary(path: &Path) -> Result<String, String> {
let xml = run(Command::new("/usr/bin/plutil")
.args(["-convert", "xml1", "-o", "-", "--"])
.arg(path))?;
let root = xml.split_once("<plist")
.and_then(|(_, rest)| rest.split_once('>'))
.map(|(_, rest)| rest.trim_start())
.ok_or_else(|| "plutil did not return a plist document".to_string())?;
if !root.starts_with("<dict>") && !root.starts_with("<dict/>") {
return Err("Info.plist must contain a dictionary".to_string());
}
Ok(xml)
}
fn run(command: &mut Command) -> Result<String, String> {
let output = command.output()
.map_err(|e| format!("Cannot run {}: {e}", command.get_program().to_string_lossy()))?;
if !output.status.success() {
return Err(format!("{} failed ({}): {}{}",
command.get_program().to_string_lossy(), output.status,
String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)));
}
String::from_utf8(output.stdout).map_err(|e| format!("Tool output is not UTF-8: {e}"))
}
struct TemporaryDirectory(PathBuf);
impl TemporaryDirectory {
fn new() -> Result<Self, String> {
// mktemp creates a private directory atomically, including for parallel
// builds. Drop cleans it up on both successful and failed merges.
let path = run(Command::new("/usr/bin/mktemp")
.args(["-d", "-t", "makepad-info-plist"]))?;
Ok(Self(PathBuf::from(path.trim_end_matches('\n'))))
}
}
impl Drop for TemporaryDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}

View file

@ -1,300 +0,0 @@
#![cfg(target_os = "macos")]
use super::AppleOs;
use super::super::compile::PlistValues;
use std::{
fs,
path::{Path, PathBuf},
process::{Command, Output},
sync::atomic::{AtomicU64, Ordering},
};
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
struct Package {
root: PathBuf,
path: PathBuf,
}
impl Package {
fn new(metadata: &str) -> Self {
let root = std::env::temp_dir().join(format!(
"makepad-info-plist-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(),
NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed),
));
fs::create_dir_all(root.join("workspace/member package/src")).unwrap();
let root = fs::canonicalize(root).unwrap();
let package = Self { path: root.join("workspace/member package"), root };
fs::write(package.path.join("src/main.rs"), "fn main() {}\n").unwrap();
package.manifest(metadata);
package
}
fn manifest(&self, metadata: &str) {
fs::write(self.path.join("Cargo.toml"), format!(
"[package]\nname = \"plist-test\"\nversion = \"0.1.0\"\n{metadata}\n"
)).unwrap();
}
fn plist(&self, relative: &str, xml: &str, binary: bool) -> PathBuf {
let path = self.path.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, xml.trim_start()).unwrap();
if binary {
checked_plutil(&path, &["-convert", "binary1"]);
}
path
}
}
impl Drop for Package {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn load(path: &Path, os: AppleOs) -> Result<Option<super::InfoPlist>, String> {
super::load(path, "plist-test", os)
}
fn generated(os: AppleOs) -> String {
PlistValues {
identifier: "org.example.plist-test".into(),
display_name: "Plist Test".into(),
name: "Plist Test".into(),
executable: "plist-test".into(),
version: "1.2.3".into(),
}.to_plist_file(os)
}
fn plist_xml(root: &str) -> String {
format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\">{root}</plist>\n")
}
fn one_entry(key: &str, value_xml: &str) -> String {
plist_xml(&format!("<dict><key>{key}</key>{value_xml}</dict>"))
}
fn plutil(path: &Path, args: &[&str]) -> Output {
Command::new("/usr/bin/plutil").args(args).arg("--").arg(path).output().unwrap()
}
fn checked_plutil(path: &Path, args: &[&str]) -> String {
let output = plutil(path, args);
assert!(output.status.success(), "plutil {args:?} {} failed: {}{}",
path.display(), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
String::from_utf8(output.stdout).unwrap()
}
fn raw(path: &Path, key: &str, expected_type: &str) -> String {
checked_plutil(path, &["-extract", key, "raw", "-expect", expected_type, "-o", "-", "-n"])
}
fn extracted_xml(path: &Path, key: &str) -> String {
checked_plutil(path, &["-extract", key, "xml1", "-o", "-"])
}
fn assert_missing(path: &Path, key: &str) {
assert!(!plutil(path, &["-extract", key, "raw", "-o", "-"]).status.success(), "unexpected key {key}");
}
fn assert_path(error: &str, path: &Path) {
assert!(error.contains(path.to_str().unwrap()), "missing path {}: {error}", path.display());
}
#[test]
fn no_opt_in_preserves_defaults_without_adding_speech_permission() {
let package = Package::new("");
for os in [AppleOs::Ios, AppleOs::Tvos] {
assert!(load(&package.path, os).unwrap().is_none());
let defaults = package.plist("Generated.plist", &generated(os), false);
checked_plutil(&defaults, &["-lint"]);
assert_missing(&defaults, "NSSpeechRecognitionUsageDescription");
assert_eq!(raw(&defaults, "CFBundleIdentifier", "string"), "org.example.plist-test");
}
}
#[test]
fn unrelated_hex_and_exponent_metadata_works_with_and_without_overlay() {
let unrelated = "[package.metadata.unrelated]\nhex_value = 0xdead_beef\nscale = 1.5e+4\n";
let package = Package::new(unrelated);
assert!(load(&package.path, AppleOs::Ios).unwrap().is_none());
package.manifest(&format!(
"{unrelated}\n[package.metadata.makepad.ios]\ninfo_plist = \"App Info.plist\""
));
package.plist("App Info.plist", &one_entry("CFBundleDisplayName", "<string>Custom App</string>"), false);
let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap();
let merged = package.plist("Merged.plist", &overlay.merge(&generated(AppleOs::Ios)).unwrap(), false);
assert_eq!(raw(&merged, "CFBundleDisplayName", "string"), "Custom App");
}
#[test]
fn selects_each_platform_relative_to_package_with_spaces_in_paths() {
let package = Package::new(
r#"[package.metadata.makepad.ios]
info_plist = '''app metadata/iOS "Bob's".plist'''
[package.metadata.makepad.tvos]
info_plist = '''app metadata/tvOS "Bob's".plist'''"#
);
for (os, filename, label) in [
(AppleOs::Ios, "app metadata/iOS \"Bob's\".plist", "Phone App"),
(AppleOs::Tvos, "app metadata/tvOS \"Bob's\".plist", "TV App"),
] {
let path = package.plist(filename, &one_entry("CFBundleDisplayName", &format!("<string>{label}</string>")), false);
let custom = load(&package.path, os).unwrap().unwrap();
assert_eq!(custom.path, path);
let merged = package.plist("Merged.plist", &custom.merge(&generated(os)).unwrap(), false);
assert_eq!(raw(&merged, "CFBundleDisplayName", "string"), label);
}
}
#[test]
fn configuring_one_platform_does_not_opt_in_the_other() {
let package = Package::new(
"[package.metadata.makepad.ios]\ninfo_plist = \"missing-ios.plist\""
);
assert!(load(&package.path, AppleOs::Tvos).unwrap().is_none());
assert!(load(&package.path, AppleOs::Ios).is_err());
package.manifest("[package.metadata.makepad.tvos]\ninfo_plist = \"missing-tvos.plist\"");
assert!(load(&package.path, AppleOs::Ios).unwrap().is_none());
assert!(load(&package.path, AppleOs::Tvos).is_err());
}
#[test]
fn xml_and_binary_overlays_preserve_types_and_unspecified_defaults() {
let package = Package::new(
"[package.metadata.makepad.ios]\ninfo_plist = \"metadata/Info.plist\""
);
let custom = plist_xml(r#"<dict>
<key>NSMicrophoneUsageDescription</key><string>Record the user's "voice" &amp; review &lt;text&gt;.</string>
<key>NSSpeechRecognitionUsageDescription</key><string>Transcribe a message.</string>
<key>LSEnvironment</key><dict><key>APP_SETTING</key><string>custom</string></dict>
<key>AppConfiguration</key><dict>
<key>Enabled</key><false/>
<key>SignedCount</key><integer>-42</integer>
<key>UnsignedCount</key><integer>18446744073709551615</integer>
<key>Payload</key><data>AAF/gP8=</data>
<key>Created</key><date>2026-09-09T12:34:56Z</date>
<key>Items</key><array><string>one &amp; &lt;two&gt;</string><real>1.5</real></array>
<key>odd: key &amp; &lt;tag&gt;</key><string>unusual key</string>
</dict>
</dict>"#);
let source = generated(AppleOs::Ios);
let defaults = package.plist("Generated.plist", &source, false);
assert_eq!(raw(&defaults, "LSEnvironment.RUST_BACKTRACE", "string"), "1");
for binary in [false, true] {
let path = package.plist("metadata/Info.plist", &custom, binary);
let original = fs::read(&path).unwrap();
let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap();
let merged = package.plist("Merged.plist", &overlay.merge(&source).unwrap(), false);
assert_eq!(fs::read(&path).unwrap(), original, "successful merge modified the source plist, binary={binary}");
checked_plutil(&merged, &["-lint"]);
for (key, value_type, expected) in [
("NSMicrophoneUsageDescription", "string", "Record the user's \"voice\" & review <text>."),
("NSSpeechRecognitionUsageDescription", "string", "Transcribe a message."),
("LSEnvironment.APP_SETTING", "string", "custom"),
("AppConfiguration.Enabled", "bool", "false"),
("AppConfiguration.SignedCount", "integer", "-42"),
("AppConfiguration.UnsignedCount", "integer", "18446744073709551615"),
("AppConfiguration.Payload", "data", "AAF/gP8="),
("AppConfiguration.Created", "date", "2026-09-09T12:34:56Z"),
("AppConfiguration.Items", "array", "2"),
("AppConfiguration.Items.0", "string", "one & <two>"),
("AppConfiguration.odd: key & <tag>", "string", "unusual key"),
] {
assert_eq!(raw(&merged, key, value_type), expected, "{key}, binary={binary}");
}
assert_eq!(raw(&merged, "AppConfiguration.Items.1", "float").parse::<f64>().unwrap(), 1.5);
for key in ["CFBundleIdentifier", "CFBundleName", "CFBundleExecutable", "UIDeviceFamily",
"UISupportedInterfaceOrientations~ipad", "UIRequiredDeviceCapabilities", "MinimumOSVersion",
"NSLocationWhenInUseUsageDescription"] {
assert_eq!(extracted_xml(&merged, key), extracted_xml(&defaults, key), "default changed: {key}, binary={binary}");
}
assert_missing(&merged, "LSEnvironment.RUST_BACKTRACE");
}
}
#[test]
fn missing_or_malformed_custom_file_reports_its_path() {
let package = Package::new(
"[package.metadata.makepad.ios]\ninfo_plist = \"missing Info.plist\""
);
let path = package.path.join("missing Info.plist");
let missing = load(&package.path, AppleOs::Ios).err().expect("missing plist must fail");
assert_path(&missing, &path);
fs::write(&path, b"<plist><dict><key>Broken</key>").unwrap();
let malformed = load(&package.path, AppleOs::Ios).err().expect("malformed plist must fail");
assert_path(&malformed, &path);
}
#[test]
fn non_dictionary_custom_root_is_rejected_in_xml_and_binary() {
let package = Package::new(
"[package.metadata.makepad.ios]\ninfo_plist = \"Info.plist\""
);
for binary in [false, true] {
let path = package.plist("Info.plist", &plist_xml("<array><string>not a dictionary</string></array>"), binary);
let error = load(&package.path, AppleOs::Ios).err().expect("array root must fail");
assert_path(&error, &path);
assert!(error.contains("dictionary"), "{error}");
}
}
#[test]
fn invalid_metadata_reports_manifest_and_key() {
let package = Package::new("");
for invalid in ["\"\"", "false", "42", "[]"] {
package.manifest(&format!("[package.metadata.makepad.ios]\ninfo_plist = {invalid}"));
let error = load(&package.path, AppleOs::Ios).err().expect("invalid path metadata must fail");
assert_path(&error, &package.path.join("Cargo.toml"));
assert!(error.contains("package.metadata.makepad.ios.info_plist"), "{error}");
}
}
#[test]
fn missing_or_malformed_manifest_reports_its_path() {
let package = Package::new("");
let path = package.path.join("Cargo.toml");
fs::remove_file(&path).unwrap();
let missing = load(&package.path, AppleOs::Ios).err().expect("missing manifest must fail");
assert_path(&missing, &path);
fs::write(&path, "[package\n").unwrap();
let malformed = load(&package.path, AppleOs::Ios).err().expect("malformed manifest must fail");
assert_path(&malformed, &path);
}
#[test]
fn rejects_changed_bundle_identity_or_executable_with_path_context() {
let package = Package::new(
"[package.metadata.makepad.ios]\ninfo_plist = \"Info.plist\""
);
for key in ["CFBundleIdentifier", "CFBundleExecutable"] {
for replacement in ["<string>different</string>", "<false/>"] {
let path = package.plist("Info.plist", &one_entry(key, replacement), false);
let original = fs::read(&path).unwrap();
let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap();
let error = overlay.merge(&generated(AppleOs::Ios)).unwrap_err();
assert_eq!(fs::read(&path).unwrap(), original, "rejected merge modified the source plist");
assert_path(&error, &path);
assert!(error.contains(key), "{error}");
}
}
}
#[test]
fn accepts_unchanged_bundle_identity_and_executable() {
let package = Package::new(
"[package.metadata.makepad.ios]\ninfo_plist = \"Info.plist\""
);
let custom = plist_xml("<dict><key>CFBundleIdentifier</key><string>org.example.plist-test</string>\
<key>CFBundleExecutable</key><string>plist-test</string>\
<key>CFBundleDisplayName</key><string>Custom App</string></dict>");
package.plist("Info.plist", &custom, false);
let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap();
let merged = package.plist("Merged.plist", &overlay.merge(&generated(AppleOs::Ios)).unwrap(), false);
assert_eq!(raw(&merged, "CFBundleIdentifier", "string"), "org.example.plist-test");
assert_eq!(raw(&merged, "CFBundleExecutable", "string"), "plist-test");
assert_eq!(raw(&merged, "CFBundleDisplayName", "string"), "Custom App");
}

View file

@ -1,5 +1,4 @@
mod compile;
mod info_plist;
mod sdk;
use crate::utils::{get_build_crate_from_args, get_package_binary_name};
use compile::*;

View file

@ -143,12 +143,6 @@ fn show_help() {
" --device=<DEVICE_NAME> The device name to use for signing/provisioning"
);
println!();
println!(" [package.metadata.makepad.ios] (or .tvos) in Cargo.toml:");
println!(" info_plist = \"packaging/ios/Info.plist\"");
println!(" Optional XML/binary plist dictionary, relative to the package directory.");
println!(" Its top-level keys replace generated defaults before icons and signing.");
println!(" CFBundleIdentifier and CFBundleExecutable must match the generated values.");
println!();
println!("Android commands:");
println!();
println!(

View file

@ -23,7 +23,6 @@ pub fn derive_widget(input: TokenStream) -> TokenStream {
area,
event,
visible,
imperative,
action_data,
uid,
cast,

View file

@ -254,12 +254,8 @@ struct AnimatorTrack {
play: Play,
/// The ease function
ease: Ease,
/// The target apply object (what we're animating to).
/// Held as a `ScriptObjectRef` for the same reason as `from_snapshot`: a
/// bare `ScriptObject` into the template dangles as soon as `script_mod`
/// re-runs (safe-area inset change, hot reload), and `interpolate_object`
/// walks it every frame.
target_apply: ScriptObjectRef,
/// The target apply object (what we're animating to)
target_apply: ScriptObject,
/// The starting values SNAPSHOT (captured/copied when animation begins)
/// This is a SEPARATE object from state_object - it must not be mutated during animation
/// Uses ScriptObjectRef to prevent GC from freeing it
@ -322,16 +318,6 @@ impl ScriptHook for Animator {
let Some(obj) = value.as_object() else {
return false;
};
// A `script_mod` re-run replaces every template object an in-flight
// track is animating against. The track's refs keep those objects
// alive, so walking them is safe, but they now describe the previous
// template — and a `Play::Loop` track would pin them for good. Drop
// the tracks instead. `current_states` is kept: the logical state is
// still meaningful, and the next `cut`/`play` re-resolves its objects
// from the new heap.
if apply.follows_script_rerun() {
self.tracks.clear();
}
let obj_ref = vm.bx.heap.new_object_ref(obj);
// Minted from the VM we are running in, so this always resolves; the
// fallback only exists because the lookup is fallible in general.
@ -376,11 +362,7 @@ impl ScriptApplyDefault for Animator {
_scope: &mut Scope,
_value: ScriptValue,
) -> Option<ScriptValue> {
// `follows_script_rerun` rather than `is_live_edit_reload`: what makes
// injecting the current state unsafe is not that the DSL changed, it
// is that `script_mod` re-ran and freed the objects `state_object` and
// `groups` point at. Both `Reload` and `Rebake` re-run it.
if apply.follows_script_rerun() || apply.is_animate() || apply.is_eval() {
if apply.is_live_edit_reload() || apply.is_animate() || apply.is_eval() {
return None;
}
@ -560,7 +542,7 @@ impl Animator {
// The snapshot must be a separate object that won't be mutated during animation.
// We sample from state_object (current animated values) or fall back to static state apply.
let vm_id = self.vm_id;
let (from_snapshot, target_apply_ref) = cx.with_script_vm_id(vm_id, |vm| {
let from_snapshot = cx.with_script_vm_id(vm_id, |vm| {
let snapshot = vm.bx.heap.new_object();
// Get the default state's apply for fallback values
@ -604,12 +586,8 @@ impl Animator {
},
);
// Create ScriptObjectRefs to prevent GC from freeing either object
// out from under the running animation.
(
vm.bx.heap.new_object_ref(snapshot),
vm.bx.heap.new_object_ref(target_apply),
)
// Create a ScriptObjectRef to prevent GC from freeing the snapshot
vm.bx.heap.new_object_ref(snapshot)
});
// Get the object before moving into track (for return value)
@ -623,7 +601,7 @@ impl Animator {
start_time: f64::NEG_INFINITY,
play,
ease,
target_apply: target_apply_ref,
target_apply,
from_snapshot,
redraw: target_state.redraw,
};
@ -874,7 +852,7 @@ impl Animator {
vm,
state_obj,
track.from_snapshot.as_object(),
track.target_apply.as_object(),
track.target_apply,
mix,
);
}

View file

@ -60,7 +60,10 @@ script_mod! {
return sdf.result
}
DesktopButtonType.WindowsMaxToggled => {
sdf.rect(c.x - sz, c.y - sz, 2. * sz, 2. * sz)
let sz = 5.
sdf.rect(c.x - sz + 1., c.y - sz - 1., 2. * sz, 2. * sz)
sdf.stroke(#f, 0.5 + 0.5 * self.draw_pass.dpi_dilate)
sdf.rect(c.x - sz - 1., c.y - sz + 1., 2. * sz, 2. * sz)
sdf.stroke(color, 0.5 + 0.5 * self.draw_pass.dpi_dilate)
return sdf.result
}

View file

@ -420,7 +420,6 @@ pub struct DropDown {
#[rust]
popup_global: PopupMenuGlobal,
#[imperative]
#[live]
selected_item: usize,

View file

@ -221,7 +221,7 @@ pub use crate::{
WidgetSet, WidgetSetIterator, WidgetUid,
},
widget_async::{
set_splash_theme, set_widget_async_trace, CxSplashVmExt, CxWidgetToScriptCallExt, ScriptAsyncCalls, SplashTheme,
set_widget_async_trace, CxSplashVmExt, CxWidgetToScriptCallExt, ScriptAsyncCalls,
ScriptAsyncId, ScriptAsyncResult, SplashVmId, MAIN_SPLASH_VM_ID,
},
widget_match_event::WidgetMatchEvent,

View file

@ -22,7 +22,6 @@ pub struct PageFlip {
layout: Layout,
#[live(false)]
lazy_init: bool,
#[imperative]
#[live]
active_page: LiveId,
#[rust]

View file

@ -552,9 +552,6 @@ impl Splash {
/// The scope object holding this Splash body's top-level definitions, via
/// the body id cached at eval time (with a pointer-identity fallback for
/// robustness).
/// The scope the script's top-level names live in: the one its body
/// ended in, since a `let`/`fn` that shadows a prelude name opens a
/// child scope the module scope cannot see into.
fn body_scope(&mut self, cx: &mut Cx) -> Option<ScriptObject> {
if self.vm_id == MAIN_SPLASH_VM_ID {
return None;
@ -563,14 +560,13 @@ impl Splash {
let body_id = self.body_id;
cx.with_script_vm_id(self.vm_id, |vm| {
let bodies = vm.bx.code.bodies.borrow();
let ended_in = |body: &ScriptBody| {
body.end_scope.as_ref().unwrap_or(&body.scope).as_object()
};
if let Some(body) = body_id.and_then(|i| bodies.get(i as usize)) {
return Some(ended_in(body));
return Some(body.scope.as_object());
}
bodies.iter().find_map(|body| match &body.source {
ScriptSource::Mod(m) if m.module_path == body_key => Some(ended_in(body)),
ScriptSource::Mod(m) if m.module_path == body_key => {
Some(body.scope.as_object())
}
_ => None,
})
})

View file

@ -99,7 +99,6 @@ pub struct View {
#[live]
event_order: EventOrder,
#[imperative]
#[live(true)]
pub visible: bool,
#[live(false)]
@ -124,15 +123,9 @@ pub struct View {
#[live]
on_render: ScriptFnRef,
/// `|index|` of the direct child a tap landed on. Lives on the container
/// so `on_render` rows stay closure-free and drag scrolling keeps working.
#[live]
on_item_tap: ScriptFnRef,
#[rust]
script_async: ScriptAsyncCalls,
#[rust]
item_tap_live: bool,
#[rust]
scroll_bars_obj: Option<Box<ScrollBars>>,
@ -927,35 +920,6 @@ impl Widget for View {
if let Some(scroll_bars) = &mut self.scroll_bars_obj {
scroll_bars.handle_scroll_event(cx, event, scope, &mut Vec::new());
}
// After the scroll bars so their drag capture comes first; the overload
// lets this view capture the same press alongside it.
if fling_caught {
self.item_tap_live = false;
} else if self.visible && self.on_item_tap.as_object() != ScriptObject::ZERO {
match event.hits_with_capture_overload(cx, self.area(), true) {
Hit::FingerDown(e) => {
// A child that already owns this press (a Button) gets the tap.
self.item_tap_live = !cx.fingers.is_digit_captured_elsewhere(e.digit_id, self.area());
}
Hit::FingerUp(e) if e.was_tap() && self.item_tap_live => {
self.item_tap_live = false;
// clipped_rect: scrolled and clipped, like the hit test itself.
let index = self.children.iter()
.position(|(_, child)| child.area().clipped_rect(cx).contains(e.abs));
if let Some(index) = index {
cx.widget_to_script_call(
uid,
NIL,
self.source.clone(),
self.on_item_tap.clone(),
&[(index as f64).into()],
);
}
}
_ => (),
}
}
}
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {

View file

@ -8,7 +8,7 @@ use {
std::any::Any,
std::cell::RefCell,
std::collections::{HashMap, VecDeque},
std::sync::atomic::{AtomicU64, AtomicU8, Ordering},
std::sync::atomic::{AtomicU64, Ordering},
};
static SCRIPT_ASYNC_COUNTER: AtomicU64 = AtomicU64::new(1);
@ -32,30 +32,6 @@ pub struct SplashVmId(pub u64);
pub const MAIN_SPLASH_VM_ID: SplashVmId = SplashVmId(0);
/// The widget theme a Splash isolate boots with. A host picks its own theme
/// after `theme_mod`, which an isolate's prelude never sees, so it says here.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum SplashTheme {
#[default]
Dark,
Light,
Skeleton,
}
static SPLASH_THEME: AtomicU8 = AtomicU8::new(0);
pub fn set_splash_theme(theme: SplashTheme) {
SPLASH_THEME.store(theme as u8, Ordering::Relaxed);
}
fn splash_theme() -> SplashTheme {
match SPLASH_THEME.load(Ordering::Relaxed) {
1 => SplashTheme::Light,
2 => SplashTheme::Skeleton,
_ => SplashTheme::Dark,
}
}
thread_local! {
/// Splash isolate VMs whose owning `Splash` widget has been dropped, awaiting
/// reclamation. `Drop` can't reach `Cx`, so it only records the id here; the
@ -418,17 +394,7 @@ impl CxSplashVmExt for Cx {
bx: Box::new(ScriptVmBase::new()),
};
crate::makepad_draw::makepad_platform::script::script_mod(&mut vm);
crate::theme_mod(&mut vm);
match splash_theme() {
SplashTheme::Light => {
vm.eval(crate::makepad_script::script! { mod.theme = mod.themes.light });
}
SplashTheme::Skeleton => {
vm.eval(crate::makepad_script::script! { mod.theme = mod.themes.skeleton });
}
SplashTheme::Dark => {}
}
crate::widgets_mod(&mut vm);
crate::script_mod(&mut vm);
// Splash isolates run untrusted-ish mini-app script; strip the
// ambient-authority modules from the isolate's namespace entirely:
// filesystem access (`fs`), child processes (`run`), and the resource

View file

@ -163,7 +163,7 @@ script_mod! {
draw_bg.button_type: DesktopButtonType.WindowsMin
width: 46 height: 29
draw_bg +: {
color: theme.color_label_inner, color_hover: #000, color_down: #000
color: #000, color_hover: #000, color_down: #000
bg_color_hover: #E9E9E9, bg_color_down: #CCCCCC
}
}
@ -171,7 +171,7 @@ script_mod! {
draw_bg.button_type: DesktopButtonType.WindowsMax
width: 46 height: 29
draw_bg +: {
color: theme.color_label_inner, color_hover: #000, color_down: #000
color: #000, color_hover: #000, color_down: #000
bg_color_hover: #E9E9E9, bg_color_down: #CCCCCC
}
}
@ -179,7 +179,7 @@ script_mod! {
draw_bg.button_type: DesktopButtonType.WindowsClose
width: 46 height: 29
draw_bg +: {
color: theme.color_label_inner, color_hover: #FFF, color_down: #FFF
color: #000, color_hover: #FFF, color_down: #FFF
bg_color_hover: #E81123, bg_color_down: #F1707A
}
}
@ -374,17 +374,13 @@ pub struct Window {
/// Used to only emit a platform op when the resolved value actually changes.
#[rust]
system_bar_dark_icons: Option<bool>,
/// Cached `(caption_bar visible, caption rect, buttons rect)` for `WindowDragQuery`. It is
/// refreshed only after layout finishes, so a synchronous native hit-test between configure
/// and redraw cannot preserve rectangles from the previous window size.
/// Cached `(caption_bar visible, caption rect, buttons rect)` for `WindowDragQuery`. That event
/// fires once per `WM_NCHITTEST` — i.e. on every mouse move on Windows — and resolving the
/// views + their areas each time runs widget-tree lookups, a real source of scroll jitter when
/// the mouse is moved during a fling. These only change on relayout, so we recompute lazily and
/// invalidate on `WindowGeomChange`.
#[rust]
drag_query_cache: Option<(bool, Rect, Rect)>,
/// Whether a completed draw has made this frame's areas authoritative, so a geometry
/// computed now may be cached. Between a configure and the redraw that answers it the
/// areas still describe the previous size, and a query in that window is answered live
/// without being stored.
#[rust]
drag_query_layout_valid: bool,
/// The caption-layout inputs (show_caption_bar, height override, system caption height) that
/// `drag_query_cache` was last computed against. When they change without a platform
/// `WindowGeomChange` (e.g. a live/DSL reload toggling the caption), we drop the cache in
@ -510,54 +506,6 @@ fn gauss_render_texture_y_flip_for_os(os_type: &OsType) -> f32 {
}
}
fn classify_window_drag_query(
visible: bool,
caption_rect: Rect,
buttons_rect: Rect,
transitional_buttons_rect: Rect,
point: Vec2d,
) -> WindowDragQueryResponse {
if !visible {
return WindowDragQueryResponse::NoAnswer;
}
let hits_buttons = (buttons_rect.size != Vec2d::default() && buttons_rect.contains(point))
|| (transitional_buttons_rect.size != Vec2d::default()
&& transitional_buttons_rect.contains(point));
if hits_buttons {
WindowDragQueryResponse::Client
} else if caption_rect.contains(point) {
WindowDragQueryResponse::Caption
} else {
WindowDragQueryResponse::NoAnswer
}
}
fn configured_window_buttons_rect(buttons_rect: Rect, configured_rect: Rect) -> Rect {
if buttons_rect.size == Vec2d::default() || configured_rect.size == Vec2d::default() {
return configured_rect;
}
Rect {
pos: dvec2(
configured_rect.pos.x + configured_rect.size.x - buttons_rect.size.x,
buttons_rect.pos.y,
),
size: buttons_rect.size,
}
}
fn configured_window_caption_rect(caption_rect: Rect, configured_size: Vec2d) -> Rect {
if caption_rect.size == Vec2d::default() || configured_size == Vec2d::default() {
return caption_rect;
}
Rect {
pos: caption_rect.pos,
size: dvec2(
(configured_size.x - caption_rect.pos.x).max(0.0),
caption_rect.size.y,
),
}
}
impl GaussStack {
fn new(cx: &mut Cx) -> Self {
let scene_pass = DrawPass::new_with_name(cx, "gauss_scene");
@ -1036,20 +984,15 @@ impl Window {
.set_visible(cx, self.show_caption_bar && !is_fullscreen);
}
OsType::LinuxWindow(params) => {
// X11 uses WM decorations. Wayland decides per window from the
// compositor's xdg-decoration configure event.
let custom_chrome = params.custom_window_chrome
&& self
.window
.handle
.uses_wayland_client_side_decorations(cx);
let visible = self.show_caption_bar
&& custom_chrome
&& !self.window.handle.is_wayland_fullscreen(cx);
// Only show the caption bar if we're drawing our own window chrome
// (e.g. Wayland without server-side decorations). On X11 the WM
// provides native decorations, so we hide the in-app caption bar.
let custom_chrome = params.custom_window_chrome;
self.view(cx, ids!(caption_bar))
.set_visible(cx, visible);
self.view(cx, ids!(windows_buttons))
.set_visible(cx, visible);
.set_visible(cx, self.show_caption_bar && custom_chrome);
if custom_chrome {
self.view(cx, ids!(windows_buttons)).set_visible(cx, true);
}
}
OsType::LinuxDirect | OsType::Android(_) => {
//self.frame.get_view(ids!(caption_bar)).set_visible(false);
@ -1061,21 +1004,6 @@ impl Window {
}
}
fn caption_drag_geometry(&self, cx: &mut Cx) -> (bool, Rect, Rect) {
// Each `self.view` is a widget-tree walk, so the caption bar is resolved once
// rather than once per field read.
let caption = self.view(cx, ids!(caption_bar));
let visible = caption.visible();
let caption_rect = caption.area().rect(cx);
let buttons = self.view(cx, ids!(windows_buttons));
let buttons_rect = if buttons.visible() {
buttons.area().rect(cx)
} else {
Rect::default()
};
(visible, caption_rect, buttons_rect)
}
fn sync_caption_bar_height(&mut self, cx: &mut Cx) {
// Explicit DSL override takes priority, then system-calculated.
let height = self
@ -1189,7 +1117,6 @@ impl Window {
if self.caption_query_sig != Some(caption_sig) {
self.caption_query_sig = Some(caption_sig);
self.drag_query_cache = None;
self.drag_query_layout_valid = false;
}
self.sync_caption_bar_state(cx);
@ -1386,14 +1313,6 @@ impl Window {
cx.end_pass_sized_turtle();
// Areas are authoritative only after this frame's layout has completed, so this is
// where a cached answer becomes allowed. Computing it here instead would charge
// every frame for three widget-tree walks that only a drag query ever reads, and
// most frames never see one. Dropping last frame's answer rather than keeping it
// means the first query after any relayout still recomputes, whether or not the
// relayout was one of the two that invalidate explicitly.
self.drag_query_cache = None;
self.drag_query_layout_valid = true;
self.main_draw_list.end(cx);
cx.end_pass(&self.pass.handle);
}
@ -1423,16 +1342,6 @@ impl Window {
self.window.handle.configure_macos_window(cx, config);
}
pub fn configure_wayland_decorations(
&mut self,
cx: &mut Cx,
preference: WaylandDecorationPreference,
) {
self.window
.handle
.configure_wayland_decorations(cx, preference);
}
pub fn window_index(&self) -> usize {
self.window.handle.window_id().id()
}
@ -1454,66 +1363,6 @@ mod tests {
1.0
);
}
#[test]
fn native_button_geometry_wins_during_configure_to_draw_transition() {
let stale_caption = Rect {
pos: dvec2(0.0, 0.0),
size: dvec2(800.0, 29.0),
};
let stale_buttons = Rect {
pos: dvec2(662.0, 0.0),
size: dvec2(138.0, 29.0),
};
let configured_buttons = Rect {
pos: dvec2(1782.0, 0.0),
size: dvec2(138.0, 29.0),
};
assert!(matches!(
classify_window_drag_query(
true,
stale_caption,
stale_buttons,
configured_buttons,
dvec2(1851.0, 14.0),
),
WindowDragQueryResponse::Client
));
assert!(matches!(
classify_window_drag_query(
true,
stale_caption,
stale_buttons,
Rect::default(),
dvec2(400.0, 14.0),
),
WindowDragQueryResponse::Caption
));
let zoomed_buttons = Rect {
pos: dvec2(708.0, 0.0),
size: dvec2(92.0, 19.0),
};
assert_eq!(
configured_window_buttons_rect(zoomed_buttons, configured_buttons),
Rect {
pos: dvec2(1828.0, 0.0),
size: dvec2(92.0, 19.0),
}
);
let configured_caption =
configured_window_caption_rect(stale_caption, dvec2(1920.0, 1080.0));
assert!(matches!(
classify_window_drag_query(
true,
configured_caption,
stale_buttons,
configured_buttons,
dvec2(1200.0, 14.0),
),
WindowDragQueryResponse::Caption
));
}
}
impl WindowRef {
@ -1629,16 +1478,6 @@ impl WindowRef {
inner.configure_macos_window(cx, config);
}
}
pub fn configure_wayland_decorations(
&self,
cx: &mut Cx,
preference: WaylandDecorationPreference,
) {
if let Some(mut inner) = self.borrow_mut() {
inner.configure_wayland_decorations(cx, preference);
}
}
}
impl Widget for Window {
@ -1699,10 +1538,8 @@ impl Widget for Window {
Event::WindowGeomChange(ev) => {
if ev.window_id == self.window.window_id() {
// The caption / buttons may have been re-laid-out; drop the WindowDragQuery
// geometry cache so it is recomputed on the next hit-test, and mark the
// areas non-authoritative until the redraw that answers this configure.
// geometry cache so it is recomputed on the next hit-test.
self.drag_query_cache = None;
self.drag_query_layout_valid = false;
match cx.os_type() {
OsType::Windows | OsType::Macos => {
if self.hide_caption_on_fullscreen && !cx.in_makepad_studio() {
@ -1763,51 +1600,42 @@ impl Widget for Window {
}
Event::WindowDragQuery(dq) => {
if dq.window_id == self.window.window_id() {
// A native query can arrive synchronously after configure but before redraw.
// Use live areas for that one query, but only a completed draw may cache them.
let cache_ready = self.drag_query_cache.is_some() || self.drag_query_layout_valid;
let geometry = match self.drag_query_cache {
Some(cached) => cached,
// Resolve the caption / buttons geometry at most once per relayout; this event
// arrives per mouse-move (per WM_NCHITTEST) and the view lookups are not free.
let (visible, caption_rect, buttons_rect) = match self.drag_query_cache {
Some(c) => c,
None => {
let live = self.caption_drag_geometry(cx);
if self.drag_query_layout_valid {
self.drag_query_cache = Some(live);
let visible = self.view(cx, ids!(caption_bar)).visible();
let caption_rect = self.view(cx, ids!(caption_bar)).area().rect(cx);
let buttons_view = self.view(cx, ids!(windows_buttons));
let buttons_visible = buttons_view.visible();
let buttons_rect = buttons_view.area().rect(cx);
// Only cache once the caption bar AND its (visible) buttons have actually
// been laid out, so an early query doesn't pin a stale rect. Pinning a
// zero buttons_rect while the buttons are visible-but-not-yet-laid-out
// would make the min/max/close strip respond as draggable Caption (a
// click on Close would drag the window) until the next geometry change. A
// window with no (hidden) buttons keeps a zero buttons_rect, which is fine.
let caption_ready = caption_rect.size != Vec2d::default();
let buttons_ready =
!buttons_visible || buttons_rect.size != Vec2d::default();
if !visible || (caption_ready && buttons_ready) {
self.drag_query_cache = Some((visible, caption_rect, buttons_rect));
}
live
(visible, caption_rect, buttons_rect)
}
};
let (visible, mut caption_rect, buttons_rect) = geometry;
if !cache_ready {
caption_rect = configured_window_caption_rect(
caption_rect,
cx.windows[dq.window_id].window_geom.inner_size,
);
}
let transitional_buttons = if cache_ready {
Rect::default()
} else {
configured_window_buttons_rect(
buttons_rect,
cx.windows[dq.window_id].window_geom.window_chrome_buttons,
)
};
match classify_window_drag_query(
visible,
caption_rect,
buttons_rect,
transitional_buttons,
dq.abs,
) {
WindowDragQueryResponse::Client => {
// Button geometry wins even if the stale caption rect still has the
// previous width, and therefore also blocks native top-edge resize.
dq.response.set(WindowDragQueryResponse::Client);
}
WindowDragQueryResponse::Caption => {
dq.response.set(WindowDragQueryResponse::Caption);
if visible {
if caption_rect.contains(dq.abs) {
if buttons_rect.size != Vec2d::default()
&& buttons_rect.contains(dq.abs)
{
dq.response.set(WindowDragQueryResponse::Client);
} else {
dq.response.set(WindowDragQueryResponse::Caption);
}
cx.set_cursor(MouseCursor::Default);
}
WindowDragQueryResponse::NoAnswer | WindowDragQueryResponse::SysMenu => {}
}
}
true