//! A shell in a browser's desktop, as crustace is on forge: a background, a bar along the top that keeps windows below it, a panel that opens over everything and takes the keyboard, and a card on a subsurface that slides without being painted again. It needs forge/web on the page; `make hello` in forge/web serves it at /shell.html, with web_hello as a window. use ltk::{ Anchor, App, ButtonVariant, Element, Layer, Length, OverlayId, OverlaySpec, ShellMode, SubsurfaceId, SubsurfaceParent, SubsurfaceSpec, button, column, container, row, spacer, text, text_edit }; use wasm_bindgen::prelude::wasm_bindgen; const BAR: OverlayId = OverlayId( 1 ); const PANEL: OverlayId = OverlayId( 2 ); const CARD: SubsurfaceId = SubsurfaceId( 1 ); /// How far the card slides, and the milliseconds it takes to come and go. const SLIDE_PX: f64 = 600.0; const SLIDE_MS: f64 = 3000.0; /// The bar's height, in density-independent pixels. const BAR_HEIGHT: f32 = 48.0; #[ derive( Clone ) ] enum Msg { TogglePanel, ClosePanel, Edit( String ), ToggleSlide, } #[ derive( Default ) ] struct Shell { panel: bool, note: String, /// When the card started sliding, in milliseconds since the epoch. sliding: Option, } impl Shell { fn bar( &self ) -> OverlaySpec { let palette = ltk::theme_palette(); let view = container( row::() .spacing( 12.0 ) .push( text( "ltk shell" ).size( 18.0 ).color( palette.text_primary ) ) .push( spacer() ) .push( button::( if self.sliding.is_some() { "Parar" } else { "Deslizar" } ).variant( ButtonVariant::Secondary ).on_press( Msg::ToggleSlide ) ) .push( button::( if self.panel { "Cerrar panel" } else { "Abrir panel" } ).variant( ButtonVariant::Secondary ).on_press( Msg::TogglePanel ) ), ) .background( palette.surface ) .padding_h( 16.0 ); OverlaySpec { id: BAR, layer: Layer::Top, anchor: Anchor::TOP, size: ( Length::px( 0.0 ), Length::dp( BAR_HEIGHT ) ), exclusive_zone: ( BAR_HEIGHT * ltk::density() ).round() as i32, keyboard_exclusive: false, input_region: None, view: view.into(), on_dismiss: None, anchor_widget_id: None, } } fn panel( &self ) -> OverlaySpec { let palette = ltk::theme_palette(); let view = container( column::() .spacing( 16.0 ) .push( text( "Panel con el teclado en exclusiva" ).size( 20.0 ).color( palette.text_primary ) ) .push( text_edit::( "Escribe aquĆ­", &self.note ).on_change( Msg::Edit ) ) .push( button::( "Cerrar" ).variant( ButtonVariant::Secondary ).on_press( Msg::ClosePanel ) ), ) .background( palette.surface_alt ) .radius( 16.0 ) .padding( 24.0 ); OverlaySpec { id: PANEL, layer: Layer::Overlay, anchor: Anchor { top: false, bottom: false, left: false, right: false }, size: ( Length::dp( 420.0 ), Length::dp( 260.0 ) ), exclusive_zone: 0, keyboard_exclusive: true, input_region: None, view: view.into(), on_dismiss: Some( Msg::ClosePanel ), anchor_widget_id: None, } } /// The card's offset along its slide, back and forth. fn card_x( &self ) -> i32 { let Some( start ) = self.sliding else { return 0 }; let phase = ( js_sys::Date::now() - start ) / SLIDE_MS * std::f64::consts::TAU; ( ( 1.0 - phase.cos() ) / 2.0 * SLIDE_PX * ltk::density() as f64 ).round() as i32 } fn card( &self ) -> SubsurfaceSpec { let palette = ltk::theme_palette(); let card = container( text( "Una subsuperficie: se mueve sin volver a pintarse" ).size( 18.0 ).color( palette.surface ) ) .background( palette.accent ) .radius( 12.0 ) .padding( 20.0 ); let density = ltk::density(); SubsurfaceSpec { id: CARD, parent: SubsurfaceParent::Main, view: column::().push( row::().push( card ) ).into(), x: ( 40.0 * density ).round() as i32 + self.card_x(), y: ( 120.0 * density ).round() as i32, content_version: 1, gpu: true, } } } impl App for Shell { type Message = Msg; fn app_id( &self ) -> &str { "net.liberux.ltk.Shell" } fn save_state( &self ) -> Option> { None } fn restore_state( &mut self, _state: Vec ) {} fn shell_mode( &self ) -> ShellMode { ShellMode::Layer( Layer::Background ) } fn view( &self ) -> Element { let palette = ltk::theme_palette(); column::() .push( spacer() ) .push( text( "El fondo del shell" ).size( 28.0 ).color( palette.text_secondary ) ) .push( spacer() ) .into() } fn overlays( &self ) -> Vec> { let mut out = vec![ self.bar() ]; if self.panel { out.push( self.panel() ); } out } fn subsurfaces( &self ) -> Vec> { vec![ self.card() ] } fn is_animating( &self ) -> bool { self.sliding.is_some() } /// Sliding moves the card's subsurface and nothing else, so no frame paints the background again. fn subsurface_motion_only( &self ) -> bool { self.sliding.is_some() } fn update( &mut self, msg: Msg ) { match msg { Msg::TogglePanel => self.panel = !self.panel, Msg::ClosePanel => self.panel = false, Msg::Edit( note ) => self.note = note, Msg::ToggleSlide => self.sliding = if self.sliding.is_some() { None } else { Some( js_sys::Date::now() ) }, } } } #[ wasm_bindgen( start ) ] pub async fn start() { if let Err( e ) = ltk::web::load_resources( "ltk" ).await { web_sys::console::warn_1( &format!( "ltk resources: {e}" ).into() ); } ltk::run( Shell::default() ); }