Menu
cosmic-mobile
publicLatest change 0523ab27b0a6bf083e8a2e5be9c526e4e0a53760 - Add glass-style launcher and reserved phone insets by AkurAI Build
use eframe::{egui::{self, *}, glow::HasContext};
use std::{process::{Child, Command}, time::{Duration,SystemTime,UNIX_EPOCH}};
mod layout;
#[derive(PartialEq)] enum Page { Home, Notes, System }
struct Shell { page: Page, notes: String, renderer: String, search: String, error: String, child: Option<Child>, wallpaper: TextureHandle }
fn clock() -> String { let n=SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); format!("{:02}:{:02}",n/3600%24,n/60%60) }
fn glass() -> Frame { Frame::new().fill(Color32::from_rgba_unmultiplied(225,231,255,32)).stroke(Stroke::new(1.,Color32::from_white_alpha(64))).corner_radius(28).inner_margin(20) }
fn icon(ui: &mut Ui, label: &str, index: usize) -> Response {
let (r,response)=ui.allocate_exact_size(vec2(76.,100.),Sense::click());
response.widget_info(||WidgetInfo::labeled(WidgetType::Button,true,label));
let tile=Rect::from_min_size(r.min+vec2(6.,0.),vec2(64.,64.));
let colors=[Color32::from_rgb(239,184,74),Color32::from_rgb(78,161,180),Color32::from_rgb(119,129,164)];
ui.painter().rect_filled(tile,18,colors[index]);
ui.painter().rect_stroke(tile,18,Stroke::new(if response.has_focus(){3.}else{1.},Color32::from_white_alpha(150)),StrokeKind::Inside);
let c=tile.center(); let st=Stroke::new(2.5,Color32::WHITE);
match index {
0=>{for y in [-9.,0.,9.] {ui.painter().line_segment([c+vec2(-13.,y),c+vec2(13.,y)],st);}},
1=>{ui.painter().line_segment([c+vec2(-14.,-10.),c+vec2(-4.,0.)],st);ui.painter().line_segment([c+vec2(-4.,0.),c+vec2(-14.,10.)],st);ui.painter().line_segment([c+vec2(1.,10.),c+vec2(14.,10.)],st);},
_=>{ui.painter().circle_stroke(c,14.,st);ui.painter().circle_stroke(c,5.,st);}
}
ui.painter().text(pos2(r.center().x,r.top()+78.),Align2::CENTER_TOP,label,FontId::proportional(14.),Color32::WHITE);
response
}
impl Shell {
fn open(&mut self,index:usize) { self.error.clear(); match index {0=>self.page=Page::Notes,2=>self.page=Page::System,_=>{
if self.child.as_mut().is_some_and(|c|matches!(c.try_wait(),Ok(None))) {self.error="Terminal already running. Close it to return.".into();return;}
match Command::new("foot").args(["--app-id=mobile-terminal","--font=monospace:size=16"]).spawn(){Ok(c)=>self.child=Some(c),Err(e)=>self.error=format!("Could not open Terminal: {e}")}
}}}
}
impl eframe::App for Shell {
fn update(&mut self,ctx:&Context,_:&mut eframe::Frame){
ctx.request_repaint_after(Duration::from_secs(1));
if ctx.input(|i|i.key_pressed(Key::Escape)){self.page=Page::Home;}
let screen=ctx.content_rect(); let [l,t,r,b]=layout::safe_area(screen.width(),screen.height());
let p=ctx.layer_painter(LayerId::background());
p.image(self.wallpaper.id(),screen,Rect::from_min_max(pos2(0.,0.),pos2(1.,1.)),Color32::WHITE);
// ponytail: conservative VM insets; replace with compositor/device cutout metadata on hardware.
p.text(pos2(l,27.),Align2::LEFT_CENTER,clock(),FontId::proportional(16.),Color32::WHITE);
p.text(pos2(r,27.),Align2::RIGHT_CENTER,"VM · UTC",FontId::proportional(12.),Color32::from_white_alpha(210));
let home=Rect::from_center_size(pos2(screen.center().x,screen.bottom()-17.),vec2(124.,5.));
p.rect_filled(home,3,Color32::from_white_alpha(230));
Area::new(Id::new("content")).fixed_pos(pos2(l,t)).show(ctx,|ui|{
ui.set_width(r-l); ui.set_max_height(b-t-120.);
ScrollArea::vertical().max_height(b-t-120.).show(ui,|ui|{
if self.page==Page::Home {
ui.add_space(24.); ui.vertical_centered(|ui|{ui.label(RichText::new(clock()).size(76.).strong());ui.label(RichText::new("Make room for what matters.").size(16.));});ui.add_space(42.);
glass().show(ui,|ui|{ui.set_width(r-l-40.);ui.add(TextEdit::singleline(&mut self.search).hint_text("Search apps").desired_width(f32::INFINITY).frame(false));});
ui.add_space(32.);
ui.horizontal_wrapped(|ui|{ui.spacing_mut().item_spacing=vec2(24.,20.);for (i,label) in ["Notes","Terminal","Settings"].iter().enumerate(){if label.to_lowercase().contains(&self.search.to_lowercase()) && icon(ui,label,i).clicked(){self.open(i);}}});
if !self.search.is_empty() && !["notes","terminal","settings"].iter().any(|n|n.contains(&self.search.to_lowercase())){ui.label("No matching apps");}
} else {
ui.add_space(16.); if ui.add_sized([88.,48.],Button::new("‹ Home").frame(false)).clicked(){self.page=Page::Home;}
ui.add_space(16.);ui.heading(if self.page==Page::Notes{"Notes"}else{"Settings"});ui.add_space(24.);
glass().show(ui,|ui|{ui.set_width(r-l-40.);
if self.page==Page::Notes {ui.label("Scratchpad · session only");ui.add_space(16.);ui.add_sized([ui.available_width(),300.],TextEdit::multiline(&mut self.notes).hint_text("Start writing…").frame(false));}
else {ui.label("Graphics");ui.label(&self.renderer);ui.add_space(20.);ui.label("Native Rust · Wayland · OpenGL");ui.label("OnePlus 13R target; VM hardware");ui.add_space(20.);ui.label("Safe area: 56 top / 34 bottom");}
});
}
if !self.error.is_empty(){ui.add_space(16.);ui.label(&self.error);}
});
});
Area::new(Id::new("dock")).fixed_pos(pos2(l,b-116.)).show(ctx,|ui|{glass().show(ui,|ui|{
ui.set_width(r-l-40.);ui.horizontal(|ui|{ui.spacing_mut().item_spacing.x=((r-l-40.)-228.)/2.;for(i,label)in ["Notes","Terminal","Settings"].iter().enumerate(){if icon(ui,label,i).clicked(){self.open(i);}}});
});});
}
}
fn main()->eframe::Result{
if std::env::var_os("WAYLAND_DISPLAY").is_none(){eprintln!("Wayland required");std::process::exit(1);}
eframe::run_native("cosmic-mobile",eframe::NativeOptions{renderer:eframe::Renderer::Glow,viewport:ViewportBuilder::default().with_fullscreen(true),..Default::default()},Box::new(|cc|{
let gl=cc.gl.as_ref().ok_or("OpenGL missing")?;
// SAFETY: eframe makes this context current during creation.
let renderer=unsafe{gl.get_parameter_string(eframe::glow::RENDERER)};
if ["llvmpipe","softpipe","swrast","swiftshader"].iter().any(|s|renderer.to_lowercase().contains(s)){return Err("Software GPU rejected".into());}
eprintln!("GPU_RENDERER={renderer}");cc.egui_ctx.set_pixels_per_point(2.);
let mut style=(*cc.egui_ctx.style()).clone();style.visuals=Visuals::dark();style.visuals.override_text_color=Some(Color32::WHITE);style.spacing.interact_size=vec2(48.,48.);style.text_styles.insert(TextStyle::Body,FontId::proportional(17.));cc.egui_ctx.set_style(style);
let mut pixels=Vec::new();for y in 0..512{for x in 0..256{let (u,v)=(x as f32/256.,y as f32/512.);let a=(-((u-0.85).powi(2)*6.+(v-0.25).powi(2)*9.)).exp();let b=(-((u-0.1).powi(2)*7.+(v-0.8).powi(2)*7.)).exp();pixels.push(Color32::from_rgb((22.+100.*a+20.*b)as u8,(29.+47.*a+74.*b)as u8,(59.+54.*a+65.*b)as u8));}}
let wallpaper=cc.egui_ctx.load_texture("dusk",ColorImage::new([256,512],pixels),TextureOptions::LINEAR);
Ok(Box::new(Shell{page:Page::Home,notes:String::new(),renderer,search:String::new(),error:String::new(),child:None,wallpaper}))
}))
}