@@ -113,6 +113,7 @@ use crate::shell_layout::ShellLayout;
113113use crate :: taskbar:: { default_apps, AppEntry } ;
114114use crate :: wallpaper:: Wallpaper ;
115115use crate :: window:: { AppId , Window , WindowHost , WindowId , WindowManager } ;
116+ use fs_settings:: SettingsApp ;
116117
117118#[ cfg( feature = "iced" ) ]
118119use fs_gui_engine_iced:: {
@@ -199,6 +200,10 @@ pub enum DesktopMessage {
199200 /// Fired by the `HotReloadWatcher` when `desktop-layout.toml` changes on disk.
200201 LayoutReloaded ,
201202
203+ // ── Embedded app messages ─────────────────────────────────────────────────
204+ /// Forward a message to the embedded Settings app.
205+ SettingsMsg ( fs_settings:: app:: Message ) ,
206+
202207 // ── No-op / async completion ──────────────────────────────────────────────
203208 Noop ,
204209}
@@ -247,6 +252,11 @@ pub struct DesktopShell {
247252 // ── Wallpaper (G1.5) ──────────────────────────────────────────────────────
248253 pub wallpaper : Wallpaper ,
249254
255+ // ── Embedded app states ───────────────────────────────────────────────────
256+ /// Lazily initialised when `AppId::Settings` is first opened.
257+ /// Rendered inline in the content area — no separate OS window.
258+ pub settings_app : Option < SettingsApp > ,
259+
250260 // ── Layout mode (G1.9: replaces tiling_active bool) ──────────────────────
251261 /// Current window layout mode: Normal | Tiling | `FocusMode`.
252262 pub layout_mode : WindowLayoutMode ,
@@ -313,6 +323,7 @@ impl Default for DesktopShell {
313323 notification_center_open : false ,
314324 clock_time : Local :: now ( ) . format ( "%H:%M" ) . to_string ( ) ,
315325 clock_date : Local :: now ( ) . format ( "%d.%m.%Y" ) . to_string ( ) ,
326+ settings_app : None ,
316327 }
317328 }
318329}
@@ -326,7 +337,12 @@ impl DesktopShell {
326337 match msg {
327338 // ── Window management ─────────────────────────────────────────────
328339 DesktopMessage :: OpenApp ( app_id) => {
329- Self :: spawn_app ( app_id) ;
340+ // Embedded apps render inline — no separate OS window.
341+ // External apps (Browser, Store, …) are future gRPC/IPC targets;
342+ // we show an in-desktop window frame for them.
343+ if app_id == AppId :: Settings && self . settings_app . is_none ( ) {
344+ self . settings_app = Some ( SettingsApp :: new ( ) ) ;
345+ }
330346 self . lifecycle_bus
331347 . app_opened ( app_id. name ( ) . to_lowercase ( ) . as_str ( ) ) ;
332348 self . active_app = Some ( app_id) ;
@@ -502,6 +518,13 @@ impl DesktopShell {
502518 }
503519 }
504520
521+ // ── Embedded app messages ─────────────────────────────────────────
522+ DesktopMessage :: SettingsMsg ( msg) => {
523+ if let Some ( app) = & mut self . settings_app {
524+ return app. update ( msg) . map ( DesktopMessage :: SettingsMsg ) ;
525+ }
526+ }
527+
505528 DesktopMessage :: Noop => { }
506529 }
507530
@@ -552,33 +575,17 @@ impl DesktopShell {
552575 _ => None ,
553576 } ;
554577 if let Some ( app) = app_id {
555- if app != AppId :: Help {
556- Self :: spawn_app ( app ) ;
578+ if app == AppId :: Settings && self . settings_app . is_none ( ) {
579+ self . settings_app = Some ( SettingsApp :: new ( ) ) ;
557580 }
558581 self . active_app = Some ( app) ;
559582 self . lifecycle_bus
560583 . app_opened ( app. name ( ) . to_lowercase ( ) . as_str ( ) ) ;
561584 }
562585 }
563586
564- /// Launch an external app binary as a detached child process.
565- fn spawn_app ( app_id : AppId ) {
566- let binary = match app_id {
567- AppId :: Browser => "fs-browser" ,
568- AppId :: Settings => "fs-settings" ,
569- AppId :: Profile => "fs-profile" ,
570- AppId :: Store => "fs-store" ,
571- AppId :: Lenses => "fs-lenses" ,
572- AppId :: Builder => "fs-builder" ,
573- AppId :: Tasks => "fs-tasks" ,
574- AppId :: Bots => "fs-bots" ,
575- AppId :: Ai => "fs-ai" ,
576- AppId :: Container => "fs-container" ,
577- AppId :: Managers => "fs-managers" ,
578- AppId :: Help => return ,
579- } ;
580- let _ = std:: process:: Command :: new ( binary) . spawn ( ) ;
581- }
587+ // spawn_app removed: all apps now render inline within the desktop.
588+ // External apps (Browser, Store, …) will connect via gRPC/IPC in future phases.
582589
583590 // ── Subscription ──────────────────────────────────────────────────────────
584591
@@ -1144,26 +1151,18 @@ impl DesktopShell {
11441151 let p = self . palette ( ) ;
11451152
11461153 let content: Element < ' _ , DesktopMessage > = if let Some ( app_id) = self . active_app {
1147- let handle = svg_icon ( app_id. icon ( ) , 48.0 , p. icon_color ) ;
1148- let icon_el: Element < ' _ , DesktopMessage > = svg ( handle) . width ( 48 ) . height ( 48 ) . into ( ) ;
1149- container (
1150- column ! [
1151- icon_el,
1152- Space :: new( ) . height( 16 ) ,
1153- text( app_id. name( ) ) . size( 20 ) . color( p. cyan) ,
1154- Space :: new( ) . height( 8 ) ,
1155- text( tr( "shell-app-launched" ) ) . size( 14 ) . color( p. muted) ,
1156- Space :: new( ) . height( 16 ) ,
1157- button( text( tr( "shell-app-relaunch" ) ) . size( 13 ) )
1158- . on_press( DesktopMessage :: OpenApp ( app_id) )
1159- . padding( [ 8 , 20 ] ) ,
1160- ]
1161- . align_x ( Alignment :: Center )
1162- . spacing ( 4 ) ,
1163- )
1164- . center_x ( Length :: Fill )
1165- . center_y ( Length :: Fill )
1166- . into ( )
1154+ match app_id {
1155+ // ── Embedded: Settings renders its full UI inline ──────────────
1156+ AppId :: Settings => {
1157+ if let Some ( settings) = & self . settings_app {
1158+ settings. view ( ) . map ( DesktopMessage :: SettingsMsg )
1159+ } else {
1160+ self . view_app_window_frame ( app_id)
1161+ }
1162+ }
1163+ // ── All other apps: in-desktop window frame ────────────────────
1164+ _ => self . view_app_window_frame ( app_id) ,
1165+ }
11671166 } else {
11681167 // Render the shell layout via IcedLayoutInterpreter when available.
11691168 let descriptor = self . shell_layout . to_layout_descriptor ( ) ;
@@ -1228,6 +1227,95 @@ impl DesktopShell {
12281227 . into ( )
12291228 }
12301229
1230+ // ── App window frame (for not-yet-embedded apps) ──────────────────────────
1231+
1232+ /// Render an in-desktop window chrome for apps that are not yet embedded.
1233+ ///
1234+ /// Shows a title bar with the app icon + name and a close button, so the
1235+ /// desktop looks like a real windowing environment even before all apps are
1236+ /// fully embedded.
1237+ fn view_app_window_frame ( & self , app_id : AppId ) -> Element < ' _ , DesktopMessage > {
1238+ let p = self . palette ( ) ;
1239+ let icon_handle = svg_icon ( app_id. icon ( ) , 24.0 , p. icon_color ) ;
1240+ let icon_el: Element < ' _ , DesktopMessage > = svg ( icon_handle) . width ( 24 ) . height ( 24 ) . into ( ) ;
1241+
1242+ let title = text ( app_id. name ( ) ) . size ( 15 ) . color ( p. cyan ) ;
1243+
1244+ // Close button: removes the active app view.
1245+ let close_btn = button ( text ( "✕" ) . size ( 14 ) . color ( p. muted ) )
1246+ . on_press ( DesktopMessage :: CloseWindow (
1247+ self . windows
1248+ . open_windows ( )
1249+ . iter ( )
1250+ . find ( |w| w. app == app_id)
1251+ . map_or ( crate :: window:: WindowId ( 0 ) , |w| w. meta . id ) ,
1252+ ) )
1253+ . padding ( [ 4 , 8 ] ) ;
1254+
1255+ let title_bar = container (
1256+ row ! [
1257+ icon_el,
1258+ Space :: new( ) . width( 8 ) ,
1259+ title,
1260+ Space :: new( ) . width( Length :: Fill ) ,
1261+ close_btn
1262+ ]
1263+ . align_y ( Alignment :: Center ) ,
1264+ )
1265+ . width ( Length :: Fill )
1266+ . padding ( [ 8 , 12 ] )
1267+ . style ( move |_| container:: Style {
1268+ background : Some ( iced:: Background :: Color ( p. bg_chrome ) ) ,
1269+ border : Border {
1270+ color : p. border_accent ,
1271+ width : 1.0 ,
1272+ radius : 0.0 . into ( ) ,
1273+ } ,
1274+ ..container:: Style :: default ( )
1275+ } ) ;
1276+
1277+ let body = container ( column ! [
1278+ Space :: new( ) . height( Length :: Fill ) ,
1279+ row![
1280+ Space :: new( ) . width( Length :: Fill ) ,
1281+ column![
1282+ svg( svg_icon( app_id. icon( ) , 64.0 , p. icon_color) )
1283+ . width( 64 )
1284+ . height( 64 ) ,
1285+ Space :: new( ) . height( 16 ) ,
1286+ text( app_id. name( ) ) . size( 22 ) . color( p. cyan) ,
1287+ Space :: new( ) . height( 8 ) ,
1288+ text( tr( "shell-app-active" ) ) . size( 13 ) . color( p. muted) ,
1289+ ]
1290+ . align_x( Alignment :: Center )
1291+ . spacing( 2 ) ,
1292+ Space :: new( ) . width( Length :: Fill ) ,
1293+ ]
1294+ . align_y( Alignment :: Center ) ,
1295+ Space :: new( ) . height( Length :: Fill ) ,
1296+ ] )
1297+ . width ( Length :: Fill )
1298+ . height ( Length :: Fill )
1299+ . style ( move |_| container:: Style {
1300+ background : Some ( iced:: Background :: Color ( p. bg_content ) ) ,
1301+ ..container:: Style :: default ( )
1302+ } ) ;
1303+
1304+ container ( column ! [ title_bar, body] . spacing ( 0 ) )
1305+ . width ( Length :: Fill )
1306+ . height ( Length :: Fill )
1307+ . style ( move |_| container:: Style {
1308+ border : Border {
1309+ color : p. border_accent ,
1310+ width : 1.0 ,
1311+ radius : 4.0 . into ( ) ,
1312+ } ,
1313+ ..container:: Style :: default ( )
1314+ } )
1315+ . padding ( 1 )
1316+ . into ( )
1317+ }
1318+
12311319 // ── Taskbar ───────────────────────────────────────────────────────────────
12321320
12331321 fn view_taskbar ( & self ) -> Element < ' _ , DesktopMessage > {
0 commit comments