From 5bd9d5865a58b33cd1360d656b4a64cd68c1d6b7 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:55:42 -0700 Subject: [PATCH] =?UTF-8?q?mobile:=20the=20standalone=20shell=20=E2=80=94?= =?UTF-8?q?=20no=20desktop=20styles,=20no=20switcher=20bar=20on=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One condition, written once: build.rs turns the `mobile_only` cfg on for `target_os = "android"` (cargo-makepad passes no features) and for `--features mobile-only` (a desktop build of the same shell). Under it the Android phone shell is the whole app: the desk bar — the style dropdown, the Desktop/Phone toggle, Light/Dark, rotate — is gone, the style menu's rows and the bar's presses are compiled out, startup is Android everywhere, and `phone:` is a logged no-op. The universal desktop build keeps every style and the bar. What the bar provided moves: Light/Dark is the shade's Dark mode tile (the same style path, so the shell and every hosted app follow); rotation is the device's. The shell keeps the platform's safe-area insets for itself: the wallpaper runs edge to edge, the status bar and the navigation band lay out inside them. Co-Authored-By: Claude Fable 5.1 --- Cargo.toml | 5 ++++ README.md | 4 +-- build.rs | 15 ++++++++++ docs/android-appcard-build.md | 12 ++++++++ src/desk/phone.rs | 10 +++++-- src/desktop_app.rs | 2 ++ src/main.rs | 43 +++++++++++++++++++++++++-- src/mobile.rs | 26 ++++++++++++---- src/mobile_app.rs | 56 +++++++++++++++++++++++++---------- src/mobile_gestures.rs | 9 ++++++ src/mobile_groups.rs | 7 ++++- src/mobile_shade.rs | 26 +++++++++------- src/mobile_surface.rs | 6 ++++ src/octosense/policy.rs | 19 ++++++++---- src/shell/menu.rs | 24 ++++++++++----- src/shell/menu/nextstep.rs | 9 +++++- 16 files changed, 220 insertions(+), 53 deletions(-) create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index b30dd753..92481673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,11 @@ makepad-photos = { git = "https://github.com/OctoSense-org/makepad.git", rev = " [features] default = [] +# The standalone mobile shell: the Android phone shell fills the window, no +# desk bar, no style switcher, no desktop styles. Android builds get it +# without the flag (build.rs turns the same `mobile_only` cfg on for +# `target_os = "android"`); a desktop build is universal unless this is on. +mobile-only = [] # Also usable on desktop for testing the same embedded apps with --module. mobile-apps = ["app-reference", "app-sheets", "app-photos", "app-appcard"] app-reference = ["dep:octosense-reference"] diff --git a/README.md b/README.md index ff9f28e7..836834ea 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Makepad otherwise derives it from the checkout directory, which may still be named `makeos`. Rebuild and relaunch after updating; Cargo regenerates the development `Info.plist` automatically. -The build target selects the startup shell automatically: Android uses the Android phone layout, iOS uses the iOS phone layout, and desktop/web builds keep Omarchy. You can still switch styles from the shell's style menu. Native phone toolbars are 48 points high and respect the window's safe-area insets. +The build target selects the shell. **Android builds are the standalone mobile shell**: the Android phone shell fills the screen inside the platform's safe-area insets, with no desk bar, no style menu and no other desktop style compiled in — Light/Dark is the **Dark mode** tile in the shade's controls, and rotation follows the device. **Desktop builds are universal** — every desktop style plus the two phone styles, switched from the bar — unless built with `cargo run --features mobile-only`, which is the same standalone shell in a phone-sized window (`build.rs` turns one `mobile_only` cfg on for that feature and for `target_os = "android"`). iOS builds still start in the iOS phone layout of the universal shell. The default desktop starts empty. AI assistant startup, background app prewarming, demo filesystem generation, and wallpaper downloads are off. **System → Quit OctoSense** closes the desktop and its hosted processes. Omarchy starts with a bundled Tokyo Night wallpaper, so the background works offline on a fresh install. Installed images in `~/.octosense/wm/themes/tokyo-night/backgrounds/` take precedence. Use `cargo run -- --download-wallpapers` to fetch the theme’s full wallpaper set; **⌘CtrlSpace** cycles installed backgrounds. Asset provenance is in [resources/wallpapers/README.md](resources/wallpapers/README.md). -Eight desktop styles are available, including **OctoSense**, a floating desktop with Liquid Glass window frames, dock, bar and popups. Press **⌘Space**, type **OctoSense**, and press Enter to select its light appearance. Click **Light / Dark** beside the style name in the top bar to switch appearances, or search for **OctoSense Dark** directly. Light uses pearl and pale aqua surfaces with dark text; Dark keeps the ink-blue palette. Both include matching versions of **Abyssal Currents**, the original oceanic wallpaper, and rounded hosted surfaces. The wallpaper switches with the appearance, fills the window with a centered crop and works offline. Desktop startup remains Omarchy; Android retains its animated background. +Eight desktop styles are available, including **OctoSense**, a floating desktop with Liquid Glass window frames, dock, bar and popups. Press **⌘Space**, type **OctoSense**, and press Enter to select its light appearance. Click **Light / Dark** beside the style name in the top bar to switch appearances, or search for **OctoSense Dark** directly. Light uses pearl and pale aqua surfaces with dark text; Dark keeps the ink-blue palette. Both include matching versions of **Abyssal Currents**, the original oceanic wallpaper, and rounded hosted surfaces. The wallpaper switches with the appearance, fills the window with a centered crop and works offline. Desktop startup remains Omarchy; Android retains its animated background. None of this bar or its styles exists in the standalone mobile shell (Android, or `--features mobile-only`). Use **⌘Space** for the menu, **⌘W** to close a tile, **⌘F** for tile fullscreen, **⌘1…0** to switch workspaces, and **⌘Shift1…0** to move the focused tile. The menu's **Learn → Keybindings** lists the inherited bindings; shortcuts for apps absent from your catalog report that the app is unavailable. diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..80771394 --- /dev/null +++ b/build.rs @@ -0,0 +1,15 @@ +//! The one place the standalone-shell condition is written. +//! +//! `mobile_only` is set for Android builds (cargo-makepad passes no features, +//! and a Cargo feature cannot be target-conditional) and for any build with +//! `--features mobile-only`. Code reads it as `#[cfg(mobile_only)]` / +//! `cfg!(mobile_only)`, never as the feature or the target directly. +fn main() { + println!("cargo:rustc-check-cfg=cfg(mobile_only)"); + let feature = std::env::var_os("CARGO_FEATURE_MOBILE_ONLY").is_some(); + let android = std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android"); + if feature || android { + println!("cargo:rustc-cfg=mobile_only"); + } + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/docs/android-appcard-build.md b/docs/android-appcard-build.md index 3c0cc602..efd5298d 100644 --- a/docs/android-appcard-build.md +++ b/docs/android-appcard-build.md @@ -25,6 +25,18 @@ on the makepad fork's AppCard framework line and needs three things the stock (`octos-app`'s `stdio_spawn`) finds it there and runs `octos serve --stdio` with `HOME=/octos-home`. +## The shell an Android build is + +Android builds are the standalone mobile shell: the Android phone shell fills +the screen inside the safe-area insets, with no desk bar (no style dropdown, +Desktop/Phone toggle, Light/Dark or rotate button) and no desktop style +compiled in — Light/Dark is the **Dark mode** tile in the shade's controls. +`build.rs` turns the `mobile_only` cfg on for `target_os = "android"`, so the +plain `cargo makepad android run` below needs no feature flag. Desktop builds +stay universal (every desktop style and the bar) unless built with +`--features mobile-only`, which is the same standalone shell in a phone-sized +window — the way to check a phone change without the phone. + ## Step by step ```sh diff --git a/src/desk/phone.rs b/src/desk/phone.rs index 6e522e8a..f5f37565 100644 --- a/src/desk/phone.rs +++ b/src/desk/phone.rs @@ -204,8 +204,12 @@ impl WmDesk { } } } - pub(super) fn draw_phone_scene(&mut self,cx:&mut Cx2d,scope:&mut Scope,screen:Rect) { + pub(super) fn draw_phone_scene(&mut self,cx:&mut Cx2d,scope:&mut Scope,full:Rect) { let state=scope.data.get_mut::().unwrap(); + // The wallpaper fills the desk; the shell lays out inside the + // platform's safe area (the notch, the system bars): the status bar + // under the notch, the navigation band above Android's gesture bar. + let screen=state.phone.insets.inset(full); state.phone.viewport=screen; // The frame's exclusion zones are rebuilt from what is drawn: cleared // once here, then every surface adds its own (the shade's sheet, the @@ -235,8 +239,8 @@ impl WmDesk { let app=mobile::app_rect(screen); self.compositor.get_or_insert_with(||BackdropCompositor::new(cx)).begin(cx); self.phone_ui.begin(); - self.phone_ui.draw_wallpaper(cx,screen,style,dark,phone.wallpaper_time); - self.compositor.as_mut().unwrap().content(screen); + self.phone_ui.draw_wallpaper(cx,full,style,dark,phone.wallpaper_time); + self.compositor.as_mut().unwrap().content(full); let home_backdrop=if style==crate::desktop::DesktopStyle::Ios && phone.openness<0.999 { Some(self.compositor.as_mut().unwrap().backdrop(cx,PhoneSurface::home_dock(screen),4.0)) }else{None}; diff --git a/src/desktop_app.rs b/src/desktop_app.rs index 6ed4ccf0..8a50e57a 100644 --- a/src/desktop_app.rs +++ b/src/desktop_app.rs @@ -97,6 +97,7 @@ impl App { state.roles = roles; state.dragging.clear(); state.style.select(style); + state.phone.shade.dark = dark; if changes_size { state.style.step(1.0); } state.layout.desktop.enabled = style.floating(); for c in state.layout.all_clients() { @@ -197,6 +198,7 @@ impl App { let style = self.state_mut().style.target; self.set_desktop_style(cx, style); } + #[cfg(not(mobile_only))] pub(super) fn open_style_menu(&mut self, cx: &mut Cx) { let anchor = if self.state_mut().style.target.mobile() { self.ui.widget(cx, ids!(phone_controls)).borrow::() diff --git a/src/main.rs b/src/main.rs index 1bde3aff..78694ccc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,12 @@ mod shell; mod theme; mod tile; +/// The standalone mobile shell: the Android phone shell fills the window +/// and nothing else is offered — no desk bar, no style switcher, no +/// desktop styles. build.rs sets the `mobile_only` cfg for Android builds +/// and for `--features mobile-only`; this is its one Rust-side name. +pub(crate) const MOBILE_ONLY: bool = cfg!(mobile_only); + use std::collections::HashMap; use binds::{match_bind_armed, WmAction}; @@ -130,9 +136,13 @@ script_mod! { flow: Down // The wallpaper layer: the theme's image (crop-to-fill) // over the theme's deep background. + // The desk bar: the shell bar on a desktop style, the + // phone's style/appearance/rotate strip on a phone + // style. The standalone shell has neither. bar := SolidView{ width: Fill height: 26 + visible: #(!MOBILE_ONLY) flow: Overlay draw_bg +: { color: mod.wm_theme.background @@ -218,8 +228,10 @@ const BAR_HEIGHT_FALLBACK: f64 = 26.0; /// The bar's height and the left padding its content starts at. macOS puts /// its traffic lights on the left; Linux and Windows put caption buttons on /// the right, where they must not push the left cluster off screen. +#[cfg(not(mobile_only))] type BarMetrics = (f64, f64); +#[cfg(not(mobile_only))] fn bar_metrics_for_geom(geom: &WindowGeom, native_mobile: bool) -> BarMetrics { if native_mobile { return (48.0, 8.0); @@ -238,7 +250,7 @@ fn bar_metrics_for_geom(geom: &WindowGeom, native_mobile: bool) -> BarMetrics { (height, pad_left) } -#[cfg(test)] +#[cfg(all(test, not(mobile_only)))] mod bar_chrome_tests { use super::*; @@ -322,6 +334,7 @@ pub struct App { tick: Timer, /// Bar height + left padding last applied from the OS window buttons. #[rust] + #[cfg(not(mobile_only))] bar_metrics: Option, /// SUPER+mouse:272/273 move & resize (tiling.lua). #[rust] @@ -2651,6 +2664,7 @@ impl App { fn shell_menu_activate(&mut self, cx: &mut Cx, target: &str) { if target=="start.documents" {self.launch_app(cx,"files");return;} if target=="start.power" {self.toggle_shell_panel(cx,BarModule::Power);return;} + #[cfg(not(mobile_only))] if let Some(name) = target.strip_prefix("desktop.") { if let Some(style) = desktop::DesktopStyle::parse(name) { if style.supports_dark() { self.state_mut().style.dark = name.ends_with("-dark"); } @@ -2865,6 +2879,26 @@ impl App { /// caption buttons affect the height only; the bar's left cluster stays /// at its normal edge inset. fn update_bar_chrome(&mut self, cx: &mut Cx, geom: &WindowGeom) { + // The standalone shell fills the window and keeps the insets for + // itself: the wallpaper runs edge to edge, the status bar and the + // navigation band sit inside the safe area (desk/phone.rs). + #[cfg(mobile_only)] + { + let i = geom.safe_area_insets; + let insets = mobile_gestures::SafeInsets { top: i.top, right: i.right, bottom: i.bottom, left: i.left }; + if self.state.is_some() && self.state_mut().phone.insets != insets { + log!("wm: safe-area insets top {} right {} bottom {} left {}", i.top, i.right, i.bottom, i.left); + self.state_mut().phone.insets = insets; + self.redraw_all(cx); + } + return; + } + #[cfg(not(mobile_only))] + self.update_desk_bar_chrome(cx, geom); + } + + #[cfg(not(mobile_only))] + fn update_desk_bar_chrome(&mut self, cx: &mut Cx, geom: &WindowGeom) { let native_mobile = cfg!(any(target_os = "ios", target_os = "android")); // Insets can change without changing the toolbar (rotation, system // navigation mode), so update them before the metrics cache check. @@ -4123,7 +4157,9 @@ impl MatchEvent for App { // activations, the flyouts' controls. match wa.cast::() { ShellBarAction::Press(module) => match module { + #[cfg(not(mobile_only))] BarModule::Appearance => self.toggle_desktop_appearance(cx), + #[cfg(not(mobile_only))] BarModule::Style => self.open_style_menu(cx), BarModule::Menu => self.toggle_launcher(cx), BarModule::Workspace(i) => { @@ -4147,7 +4183,9 @@ impl MatchEvent for App { }, ShellBarAction::RightPress(module) => match module { // The Omarchy button's right click opens a terminal. + #[cfg(not(mobile_only))] BarModule::Appearance => self.toggle_desktop_appearance(cx), + #[cfg(not(mobile_only))] BarModule::Style => self.open_style_menu(cx), BarModule::Menu => self.do_action(cx, WmAction::LaunchTerminal), BarModule::ActiveWindow => { @@ -4374,7 +4412,7 @@ impl AppMain for App { // The shell bar's own modules are BUTTONS, not a drag handle: // where it claims a point, the press reaches the widget. let bar = self.ui.view(cx, ids!(bar)).area(); - if cfg!(any(target_os = "ios", target_os = "android")) + if cfg!(any(target_os = "ios", target_os = "android")) || MOBILE_ONLY || self.phone_toolbar_hit(cx,dq.abs).is_some() || self.shell_bar_claims(cx, dq.abs) { dq.response.set(WindowDragQueryResponse::Client); } else if bar.is_valid(cx) && bar.rect(cx).contains(dq.abs) { @@ -4383,6 +4421,7 @@ impl AppMain for App { dq.response.set(WindowDragQueryResponse::Client); } } + #[cfg(not(mobile_only))] if self.state.is_some() && !self.state_mut().style.target.mobile() { if let Event::MouseDown(e)=event { let module=self.ui.widget(cx,ids!(shell_bar)).borrow::().and_then(|b|b.module_at(e.abs)); diff --git a/src/mobile.rs b/src/mobile.rs index b088de09..36a5e5c2 100644 --- a/src/mobile.rs +++ b/src/mobile.rs @@ -1,6 +1,6 @@ //! Phone shell state and geometry. Application viewports remain stable while //! their compositor surfaces move between home, foreground and the task viewer. -use crate::{desktop::DesktopStyle, hub::ClientId, mobile_tiles::HomeTiles}; +use crate::{desktop::DesktopStyle, hub::ClientId, mobile_gestures::SafeInsets, mobile_tiles::HomeTiles}; use makepad_widgets::*; use std::collections::HashMap; @@ -10,7 +10,13 @@ pub enum PhoneScreen { #[default] Home, App, Recents, Drawer } #[derive(Clone, Debug, PartialEq)] pub enum PhoneHit { App(String), Card(ClientId), Home, Recents, Drawer, Back, - Rotate, Style, Appearance, Desktop, Key(String), Shift, Symbols, HideKeyboard, + /// The desk bar's phone strip (universal builds only): rotate the + /// window, the style menu, Light/Dark, back to the desktop. + #[cfg(not(mobile_only))] Rotate, + #[cfg(not(mobile_only))] Style, + #[cfg(not(mobile_only))] Appearance, + #[cfg(not(mobile_only))] Desktop, + Key(String), Shift, Symbols, HideKeyboard, ClearSearch, CancelSearch, Shade(crate::mobile_shade::ShadeHit), /// A page indicator dot: jump the home pager there (mobile_pages.rs). @@ -58,10 +64,15 @@ pub struct PhoneState { pub ime: HashMap, pub shift: bool, pub symbols: bool, - pub desktop_size: Option, - pub desktop_clients: Vec, - pub desktop_style: DesktopStyle, + /// What the desk bar's Desktop toggle restores (universal builds only). + #[cfg(not(mobile_only))] pub desktop_size: Option, + #[cfg(not(mobile_only))] pub desktop_clients: Vec, + #[cfg(not(mobile_only))] pub desktop_style: DesktopStyle, + /// The rect the shell lays out in: the desk's rect inside `insets`. pub viewport: Rect, + /// The platform's safe-area insets (the notch, the system bars): the + /// wallpaper runs under them, everything else stays inside. + pub insets: SafeInsets, /// The home page's live app tiles (mobile_tiles.rs): which client shows /// which tile and in which face. pub tiles: HomeTiles, @@ -87,7 +98,10 @@ impl Default for PhoneState { keyboard: 0.0, keyboard_target: 0.0, keyboard_sent_height: 0.0, keyboard_client: None, search_query: String::new(), search_focused: false, search_scroll: 0.0, ime: HashMap::new(), shift: false, symbols: false, - desktop_size: None, desktop_clients: Vec::new(), desktop_style: DesktopStyle::Omarchy, viewport: Rect::default(), + #[cfg(not(mobile_only))] desktop_size: None, + #[cfg(not(mobile_only))] desktop_clients: Vec::new(), + #[cfg(not(mobile_only))] desktop_style: DesktopStyle::Omarchy, + viewport: Rect::default(), insets: SafeInsets::default(), tiles: HomeTiles::default(), gesture_out: None, exclusions: Default::default(), diff --git a/src/mobile_app.rs b/src/mobile_app.rs index 545ec283..a6c4e4a9 100644 --- a/src/mobile_app.rs +++ b/src/mobile_app.rs @@ -1,6 +1,6 @@ //! Phone navigation/input, sharing the WM's real clients and launch paths. use crate::{mobile::*, mobile_surface::PhoneSurface, mobile_tiles::{self, Face, TILE_APPS}, *}; -use crate::mobile_shade::ShadeState; +use crate::mobile_shade::{ShadeHit, ShadeState, Toggle}; use crate::mobile_gestures::{Dir, FingerPhase, GestureContext, GestureKind, SafeInsets, ShellGesture}; use makepad_widgets::makepad_platform::ime::{HostedKeyboard, InputMode}; use makepad_widgets::widget_async::{enter_isolate, leave_isolate}; @@ -238,16 +238,27 @@ impl App { pub(super) fn configure_phone_mode(&mut self,cx:&mut Cx,previous:desktop::DesktopStyle,style:desktop::DesktopStyle) { let window=self.ui.window(cx,ids!(main_window)); if style.mobile() && !previous.mobile() { - let size=window.get_inner_size(cx); let focused=self.state_mut().layout.focused_client(); - let desktop_clients=self.state_mut().layout.all_clients(); + #[cfg(not(mobile_only))] + { + let size=window.get_inner_size(cx); + let desktop_clients=self.state_mut().layout.all_clients(); + let phone=&mut self.state_mut().phone; + phone.desktop_size=Some(size);phone.desktop_style=previous; + phone.desktop_clients=desktop_clients; + } let phone=&mut self.state_mut().phone; - phone.desktop_size=Some(size);phone.desktop_style=previous; - phone.desktop_clients=desktop_clients; phone.client=focused;phone.navigate(PhoneScreen::Home); phone.openness=0.0;phone.overview=0.0; + // A desktop window becomes phone-sized; a phone is its screen. if !cfg!(any(target_os="ios",target_os="android")) {window.resize(cx,phone_size(style));} }else if !style.mobile() && previous.mobile() { + // The standalone shell never leaves the phone style: nothing + // offers a desktop one, and a stray request changes nothing. + #[cfg(mobile_only)] + { log!("wm: the standalone shell stays the phone shell (asked for {:?})", style); return; } + #[cfg(not(mobile_only))] + { self.dismiss_phone_keyboard(cx); self.restore_tile_faces(cx); let size=self.state_mut().phone.desktop_size.take().unwrap_or(dvec2(1400.0,900.0)); @@ -259,6 +270,7 @@ impl App { state.layout.desktop.windows.retain(|w|retained.contains(&w.client)); let area=LRect::new(0.0,36.0,size.x,(size.y-90.0).max(1.0)); for client in state.layout.all_clients() {state.layout.desktop.ensure(client,area);} + } }else if style.mobile() && previous!=style { let current=window.get_inner_size(cx); let size=phone_size(style); @@ -266,14 +278,18 @@ impl App { window.resize(cx,if current.x>current.y {dvec2(size.y,size.x)}else{size}); } } - self.ui.widget(cx,ids!(desktop_controls)).set_visible(cx,!style.mobile()); - self.ui.widget(cx,ids!(phone_controls)).set_visible(cx,style.mobile()); + #[cfg(not(mobile_only))] + { + self.ui.widget(cx,ids!(desktop_controls)).set_visible(cx,!style.mobile()); + self.ui.widget(cx,ids!(phone_controls)).set_visible(cx,style.mobile()); + } self.ui.widget(cx,ids!(shell_ai_pane)).set_visible(cx,!style.mobile()); self.phone_time=0.0; self.animate_phone(cx); } + /// A hit on the desk bar's phone strip (there is none in the standalone shell). pub(super) fn phone_toolbar_hit(&self,cx:&Cx,p:Vec2d)->Option { - if !self.state.as_ref().is_some_and(|s|s.style.target.mobile()) {return None;} + if MOBILE_ONLY || !self.state.as_ref().is_some_and(|s|s.style.target.mobile()) {return None;} self.ui.widget(cx,ids!(phone_controls)).borrow::().and_then(|s|s.hit(p)) } pub(super) fn phone_animation_event(&mut self,cx:&mut Cx,event:&Event) { @@ -364,6 +380,15 @@ impl App { self.state_mut().phone.keyboard_target=0.0; self.animate_phone(cx); } + /// Light <-> Dark for the phone shell and every hosted app, keeping the + /// App Library's search field focused if it was. + fn toggle_phone_appearance(&mut self,cx:&mut Cx) { + let focused=self.state_mut().phone.search_focused; + self.toggle_desktop_appearance(cx); + if focused { + if let Some(mut desk)=self.desk(cx).borrow_mut::() {desk.focus_phone_search(cx,&mut self.state_mut().phone);} + } + } fn phone_action(&mut self,cx:&mut Cx,hit:PhoneHit) { match hit { PhoneHit::App(app)=>{ @@ -391,6 +416,7 @@ impl App { PhoneHit::Recents=>self.state_mut().phone.navigate(PhoneScreen::Recents), PhoneHit::Drawer=>self.state_mut().phone.navigate(PhoneScreen::Drawer), PhoneHit::Page(n)=>self.state_mut().phone.pages.jump(n), + #[cfg(not(mobile_only))] PhoneHit::Rotate=>{ let window=self.ui.window(cx,ids!(main_window));let size=window.get_inner_size(cx); self.state_mut().phone.gesture=None; @@ -399,14 +425,11 @@ impl App { self.state_mut().phone.gesture_out=None; window.resize(cx,dvec2(size.y,size.x)); } + #[cfg(not(mobile_only))] PhoneHit::Style=>self.open_style_menu(cx), - PhoneHit::Appearance=>{ - let focused=self.state_mut().phone.search_focused; - self.toggle_desktop_appearance(cx); - if focused { - if let Some(mut desk)=self.desk(cx).borrow_mut::() {desk.focus_phone_search(cx,&mut self.state_mut().phone);} - } - } + #[cfg(not(mobile_only))] + PhoneHit::Appearance=>self.toggle_phone_appearance(cx), + #[cfg(not(mobile_only))] PhoneHit::Desktop=>{let style=self.state_mut().phone.desktop_style;self.set_desktop_style(cx,style);} PhoneHit::HideKeyboard=>self.dismiss_phone_keyboard(cx), PhoneHit::ClearSearch=>{ @@ -422,6 +445,9 @@ impl App { if self.state_mut().phone.keyboard_target>0.0 {self.dismiss_phone_keyboard(cx);} else {self.phone_back(cx);} } + // The shade's Dark mode tile is the appearance the desk bar's + // Light/Dark used to set; the shade keeps every other toggle. + PhoneHit::Shade(ShadeHit::Toggle(Toggle::DarkMode))=>self.toggle_phone_appearance(cx), PhoneHit::Shade(hit)=>self.state_mut().phone.shade.tap(hit), PhoneHit::Island(hit)=>{if let Some(app)=self.island_hit(hit) {self.phone_action(cx,PhoneHit::App(app));}} } diff --git a/src/mobile_gestures.rs b/src/mobile_gestures.rs index 79826cdf..f9f5ee9d 100644 --- a/src/mobile_gestures.rs +++ b/src/mobile_gestures.rs @@ -102,6 +102,15 @@ pub enum FingerPhase { Down, Move, Up } /// the glass goes home like the system's own gesture would. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct SafeInsets { pub top: f64, pub right: f64, pub bottom: f64, pub left: f64 } +impl SafeInsets { + /// `r` with the insets taken off each edge (never below a point). + pub fn inset(&self, r: Rect) -> Rect { + Rect { + pos: r.pos + dvec2(self.left, self.top), + size: dvec2((r.size.x - self.left - self.right).max(1.0), (r.size.y - self.top - self.bottom).max(1.0)), + } + } +} /// What the recognizer knows about the screen when a finger arrives: the /// phone viewport, the insets around it and which shell screen is showing diff --git a/src/mobile_groups.rs b/src/mobile_groups.rs index 83d292dc..be8fb27b 100644 --- a/src/mobile_groups.rs +++ b/src/mobile_groups.rs @@ -496,10 +496,15 @@ impl crate::App { .map(|(c, _)| *c).min() } /// `--test-action phone:` puts the desktop into that phone - /// shell; `group:` opens a group's window; `split:,` + /// shell (a no-op in the standalone shell, which is already the one + /// phone); `group:` opens a group's window; `split:,` /// launches both apps and enters a split. pub(super) fn groups_test_action(&mut self, cx: &mut Cx, name: &str) -> bool { if let Some(style) = name.strip_prefix("phone:") { + if crate::MOBILE_ONLY { + log!("wm: --test-action phone:{} is a no-op in the standalone shell", style); + return true; + } let style = if style.eq_ignore_ascii_case("ios") { DesktopStyle::Ios } else { DesktopStyle::Android }; log!("wm: --test-action phone {:?}", style); self.set_desktop_style(cx, style); diff --git a/src/mobile_shade.rs b/src/mobile_shade.rs index 0f71041a..8722e3ec 100644 --- a/src/mobile_shade.rs +++ b/src/mobile_shade.rs @@ -32,13 +32,15 @@ pub struct ShadeNote { pub revealed: bool, } -/// The quick-settings toggles, in grid order. +/// The quick-settings toggles, in grid order. `DarkMode` is the shell's +/// appearance (what the desk bar's Light/Dark set): the app flips it +/// through its style path and mirrors the result into `ShadeState::dark`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Toggle { Wifi, Bluetooth, Torch, RotationLock, DoNotDisturb } +pub enum Toggle { Wifi, Bluetooth, Torch, RotationLock, DoNotDisturb, DarkMode } impl Toggle { - pub const ALL: [Toggle; 5] = [Toggle::Wifi, Toggle::Bluetooth, Toggle::Torch, Toggle::RotationLock, Toggle::DoNotDisturb]; + pub const ALL: [Toggle; 6] = [Toggle::Wifi, Toggle::Bluetooth, Toggle::Torch, Toggle::RotationLock, Toggle::DoNotDisturb, Toggle::DarkMode]; fn label(self) -> &'static str { - match self { Toggle::Wifi => "Wi-Fi", Toggle::Bluetooth => "Bluetooth", Toggle::Torch => "Torch", Toggle::RotationLock => "Rotation", Toggle::DoNotDisturb => "DND" } + match self { Toggle::Wifi => "Wi-Fi", Toggle::Bluetooth => "Bluetooth", Toggle::Torch => "Torch", Toggle::RotationLock => "Rotation", Toggle::DoNotDisturb => "DND", Toggle::DarkMode => "Dark mode" } } fn index(self) -> usize { Toggle::ALL.iter().position(|t| *t == self).unwrap_or(0) } } @@ -85,8 +87,11 @@ pub struct ShadeState { pub torch: bool, pub rotation_lock: bool, pub do_not_disturb: bool, + /// The shell's appearance, mirrored from `WmState::style.dark` by the + /// app whenever it changes (the tile reads it; tapping goes to the app). + pub dark: bool, /// Per-toggle 0..1 shape animation (pill → rounded rectangle). - toggle_anim: [f64; 5], + toggle_anim: [f64; 6], /// Battery percent and charging, from the status sampler the bar uses. pub battery: Option<(u32, bool)>, /// Seconds since app start at the last frame, for the cards' ages. @@ -105,8 +110,8 @@ impl Default for ShadeState { Self { open: 0.0, open_target: 0.0, side: ShadeSide::Notifications, page: 0.0, page_target: 0.0, notifications: Vec::new(), next_id: 0, - brightness: 0.62, volume: 0.45, wifi: true, bluetooth: false, torch: false, rotation_lock: false, do_not_disturb: false, - toggle_anim: [1.0, 0.0, 0.0, 0.0, 0.0], + brightness: 0.62, volume: 0.45, wifi: true, bluetooth: false, torch: false, rotation_lock: false, do_not_disturb: false, dark: false, + toggle_anim: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0], battery: None, now: 0.0, pulling: false, drag: None, dragging_note: None, seeded: false, } } @@ -124,10 +129,10 @@ impl ShadeState { /// island reads to hide, so it still docks with a pull in progress. pub fn wants_open(&self) -> bool { self.open_target > 0.5 } pub fn toggled(&self, t: Toggle) -> bool { - match t { Toggle::Wifi => self.wifi, Toggle::Bluetooth => self.bluetooth, Toggle::Torch => self.torch, Toggle::RotationLock => self.rotation_lock, Toggle::DoNotDisturb => self.do_not_disturb } + match t { Toggle::Wifi => self.wifi, Toggle::Bluetooth => self.bluetooth, Toggle::Torch => self.torch, Toggle::RotationLock => self.rotation_lock, Toggle::DoNotDisturb => self.do_not_disturb, Toggle::DarkMode => self.dark } } fn flip(&mut self, t: Toggle) { - match t { Toggle::Wifi => self.wifi = !self.wifi, Toggle::Bluetooth => self.bluetooth = !self.bluetooth, Toggle::Torch => self.torch = !self.torch, Toggle::RotationLock => self.rotation_lock = !self.rotation_lock, Toggle::DoNotDisturb => self.do_not_disturb = !self.do_not_disturb } + match t { Toggle::Wifi => self.wifi = !self.wifi, Toggle::Bluetooth => self.bluetooth = !self.bluetooth, Toggle::Torch => self.torch = !self.torch, Toggle::RotationLock => self.rotation_lock = !self.rotation_lock, Toggle::DoNotDisturb => self.do_not_disturb = !self.do_not_disturb, Toggle::DarkMode => self.dark = !self.dark } } /// Open on `side`, animated; the page follows the side. @@ -549,7 +554,8 @@ fn draw_controls(cx: &mut Cx2d, d: &mut ShellDraw, chrome: &mut DrawDesktopChrom Toggle::Bluetooth => if shade.bluetooth { Ico::Bluetooth } else { Ico::BluetoothOff }, Toggle::Torch => Ico::Power, Toggle::RotationLock => Ico::Lock, - Toggle::DoNotDisturb => Ico::Moon, + Toggle::DoNotDisturb => Ico::BellOff, + Toggle::DarkMode => Ico::Moon, }; d.icon_centered(cx, ico, rect(cell.pos.x + 10.0, cell.pos.y, 30.0, cell.size.y), 18.0, fg); let text = rect(cell.pos.x + 42.0, cell.pos.y, cell.size.x - 50.0, cell.size.y); diff --git a/src/mobile_surface.rs b/src/mobile_surface.rs index 18fb1e38..34e2a51a 100644 --- a/src/mobile_surface.rs +++ b/src/mobile_surface.rs @@ -506,10 +506,16 @@ impl PhoneSurface { } } impl Widget for PhoneSurface { + /// As a widget in the tree this surface is the desk bar's phone strip + /// (style menu, Desktop, Light/Dark, rotate). The standalone shell has + /// no bar: the desk draws the surface's home and overlay directly. fn draw_walk(&mut self,cx:&mut Cx2d,scope:&mut Scope,walk:Walk)->DrawStep { if !self.visible {self.hits.clear();self.area=Area::Empty;return DrawStep::done();} let r=cx.walk_turtle_with_area(&mut self.area,walk); self.hits.clear(); + #[cfg(mobile_only)] + let _=(r,scope); + #[cfg(not(mobile_only))] if let Some(state)=scope.data.get_mut::() { if state.style.target.mobile() { self.d.solid(cx,r,rgb(25,27,38)); diff --git a/src/octosense/policy.rs b/src/octosense/policy.rs index 54d5876c..920ff96f 100644 --- a/src/octosense/policy.rs +++ b/src/octosense/policy.rs @@ -4,12 +4,14 @@ pub fn requested(flag: &str) -> bool { } /// Select the shell for the build target, including when cross-compiling. +/// The standalone build has one shell, whatever it runs on. pub fn startup_style() -> crate::desktop::DesktopStyle { - startup_style_for_os(std::env::consts::OS) + startup_style_for_os(std::env::consts::OS, crate::MOBILE_ONLY) } -fn startup_style_for_os(target_os: &str) -> crate::desktop::DesktopStyle { +fn startup_style_for_os(target_os: &str, mobile_only: bool) -> crate::desktop::DesktopStyle { use crate::desktop::DesktopStyle; + if mobile_only { return DesktopStyle::Android; } match target_os { "android" => DesktopStyle::Android, "ios" => DesktopStyle::Ios, @@ -24,14 +26,21 @@ mod tests { #[test] fn mobile_targets_start_with_their_touch_shell() { - assert_eq!(startup_style_for_os("android"), DesktopStyle::Android); - assert_eq!(startup_style_for_os("ios"), DesktopStyle::Ios); + assert_eq!(startup_style_for_os("android", false), DesktopStyle::Android); + assert_eq!(startup_style_for_os("ios", false), DesktopStyle::Ios); + } + + #[test] + fn the_standalone_shell_is_android_everywhere() { + for target_os in ["android", "ios", "macos", "linux", "windows"] { + assert_eq!(startup_style_for_os(target_os, true), DesktopStyle::Android); + } } #[test] fn desktop_and_web_keep_the_existing_default() { for target_os in ["macos", "windows", "linux", "freebsd", "unknown"] { - assert_eq!(startup_style_for_os(target_os), DesktopStyle::Omarchy); + assert_eq!(startup_style_for_os(target_os, false), DesktopStyle::Omarchy); } } } diff --git a/src/shell/menu.rs b/src/shell/menu.rs index df6bcbf6..64f1594d 100644 --- a/src/shell/menu.rs +++ b/src/shell/menu.rs @@ -468,6 +468,8 @@ impl MenuModel { ("workspace.tools.task", "Processes", MenuKind::App), ("workspace.desktop", "Appearance", MenuKind::Menu), ] { + // Appearance routes to the style rows the standalone shell lacks. + if crate::MOBILE_ONLY && id == "workspace.desktop" { continue; } items.push(MenuItem::new(id, label, kind)); } } @@ -495,13 +497,17 @@ impl MenuModel { items.retain(|item| item.id != "workspace.tools"); } items.extend(available); - items.push(MenuItem::new("desktop","Desktop style",MenuKind::Menu)); - for style in crate::desktop::DesktopStyle::ALL { - items.push(MenuItem::new(&format!("desktop.{}",style.id()),style.label(),MenuKind::Action)); + // The style rows: the standalone shell has one style and no menu for it. + #[cfg(not(mobile_only))] + { + items.push(MenuItem::new("desktop","Desktop style",MenuKind::Menu)); + for style in crate::desktop::DesktopStyle::ALL { + items.push(MenuItem::new(&format!("desktop.{}",style.id()),style.label(),MenuKind::Action)); + } + items.push(MenuItem::new("desktop.macos-dark","macOS · Dark",MenuKind::Action)); + items.push(MenuItem::new("desktop.windows-dark","Windows · Dark",MenuKind::Action)); + items.push(MenuItem::new("desktop.octosense-dark","OctoSense · Dark",MenuKind::Action)); } - items.push(MenuItem::new("desktop.macos-dark","macOS · Dark",MenuKind::Action)); - items.push(MenuItem::new("desktop.windows-dark","Windows · Dark",MenuKind::Action)); - items.push(MenuItem::new("desktop.octosense-dark","OctoSense · Dark",MenuKind::Action)); if path.starts_with("style.theme") { items.extend(theme_items()); } @@ -1628,8 +1634,10 @@ mod tests { fn browsing_shows_direct_children_in_order() { let mut m = MenuModel::default(); m.open_at("", MenuSkin::Menu); - assert_eq!(m.rows.len(), 5); - assert!(m.rows.iter().any(|r| r.target == "desktop")); + // The standalone shell offers no Desktop style row. + let styles = if crate::MOBILE_ONLY { 0 } else { 1 }; + assert_eq!(m.rows.len(), 4 + styles); + assert_eq!(m.rows.iter().any(|r| r.target == "desktop"), styles == 1); assert_eq!(m.rows[0].label, "Apps"); assert_eq!(m.rows[3].label, "System"); assert!(m.rows.iter().all(|r| !r.disabled)); diff --git a/src/shell/menu/nextstep.rs b/src/shell/menu/nextstep.rs index ca545557..0313c7b4 100644 --- a/src/shell/menu/nextstep.rs +++ b/src/shell/menu/nextstep.rs @@ -323,7 +323,14 @@ mod tests { available.iter().map(|app| &app.id).collect::>()); assert!(model.rows.iter().all(|row| !row.disabled)); assert!(model.back()); - model.sel = model.rows.iter().position(|row| row.target == "workspace.desktop").unwrap(); + // The style rows are the universal build's; the standalone shell + // has one style and its workspace menu no Desktop entry to route to. + let desktop = model.rows.iter().position(|row| row.target == "workspace.desktop"); + if crate::MOBILE_ONLY { + assert!(desktop.is_none(), "no style rows in the standalone shell"); + return; + } + model.sel = desktop.unwrap(); model.activate(); assert_eq!(model.path, "desktop"); assert!(model.rows.iter().any(|row| row.target == "desktop.nextstep"));