Menu
cosmic-mobile
publicLatest change 7af1088dba006a3663755ef46c05922f753f4ef0 - Format multitasking integration by AkurAI Build
use eframe::{egui::*, 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)))
{
match Command::new("niri")
.args(["msg", "action", "focus-workspace", "apps"])
.status()
{
Ok(s) if s.success() => {}
_ => self.error = "Could not switch to apps".into(),
};
return;
}
match Command::new("alacritty")
.args(["--class", "Alacritty"])
.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 - 156.);
ScrollArea::vertical()
.max_height(b - t - 156.)
.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(16.);
if ui
.add_sized([ui.available_width(), 48.], Button::new("Running apps"))
.clicked()
{
if !Command::new("niri")
.args(["msg", "action", "open-overview"])
.status()
.is_ok_and(|s| s.success())
{
self.error = "App switcher unavailable".into();
}
}
ui.add_space(16.);
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 - 140.))
.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(false),
..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}");
// Compositor supplies display scale.
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,
}))
}),
)
}