Lightningbeam/lightningbeam-ui/lightningbeam-editor/src/mobile/intent.rs

172 lines
7.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Mobile new-file intent picker — the phone equivalent of the desktop start screen.
//!
//! Mirrors the desktop "new project" flow: each intent calls
//! [`EditorApp::create_new_project_with_focus`] (which builds the document, picks a default layer,
//! and switches to editor mode) and then sets the initial mobile stack window so the relevant panes
//! are in view.
use eframe::egui;
use super::icons;
use crate::EditorApp;
const C_DEVICE: egui::Color32 = egui::Color32::from_rgb(0x14, 0x16, 0x1b);
const C_CARD: egui::Color32 = egui::Color32::from_rgb(0x1f, 0x24, 0x2c);
const C_CARD_HOT: egui::Color32 = egui::Color32::from_rgb(0x27, 0x2d, 0x37);
const C_LINE: egui::Color32 = egui::Color32::from_rgb(0x36, 0x3d, 0x49);
const C_BRIGHT: egui::Color32 = egui::Color32::from_rgb(0xea, 0xee, 0xf3);
const C_DIM: egui::Color32 = egui::Color32::from_rgb(0x8b, 0x95, 0xa1);
/// One intent card.
struct Intent {
label: &'static str,
icon: &'static str,
accent: egui::Color32,
/// Argument to `create_new_project_with_focus` (0=Animation, 1=Video, 2=Music, 5=Painting).
focus: usize,
/// Initial mobile stack window: (window_top, window_count).
window: (usize, usize),
}
fn intents() -> [Intent; 6] {
let coral = egui::Color32::from_rgb(0xe8, 0x82, 0x6b);
let cyan = egui::Color32::from_rgb(0x54, 0xc3, 0xe8);
let amber = egui::Color32::from_rgb(0xf4, 0xa3, 0x40);
let pink = egui::Color32::from_rgb(0xc7, 0x5b, 0x8a);
let violet = egui::Color32::from_rgb(0x6a, 0x46, 0x90);
[
// Stage indices (see super::STACK): Stage=2, Timeline=3, PianoRoll=4, VirtualPiano=5.
Intent { label: "Draw", icon: icons::BRUSH, accent: coral, focus: 5, window: (2, 1) },
Intent { label: "Animate", icon: icons::FILM, accent: cyan, focus: 0, window: (2, 2) },
Intent { label: "Compose", icon: icons::MUSIC, accent: amber, focus: 2, window: (3, 3) },
Intent { label: "Record", icon: icons::MIC, accent: pink, focus: 2, window: (3, 3) },
Intent { label: "Edit video", icon: icons::CLAPPERBOARD, accent: violet, focus: 1, window: (2, 2) },
Intent { label: "Blank", icon: icons::SQUARE_DASHED, accent: C_DIM, focus: 0, window: (2, 2) },
]
}
pub fn render(app: &mut EditorApp, ctx: &egui::Context) {
egui::CentralPanel::default()
.frame(egui::Frame::NONE.fill(C_DEVICE))
.show(ctx, |ui| {
let rect = ui.available_rect_before_wrap();
let margin = 16.0;
let left = rect.left() + margin;
let right = rect.right() - margin;
// Header.
ui.painter().text(
egui::pos2(left, rect.top() + 22.0),
egui::Align2::LEFT_TOP,
"Start something",
egui::FontId::proportional(22.0),
C_BRIGHT,
);
// Vertical budget: ~2/3 for the intent grid, ~1/3 for the recent list.
let content_top = rect.top() + 62.0;
let content_h = (rect.bottom() - margin) - content_top;
let gap = 10.0;
let grid_h = content_h * 0.66;
// 2×3 grid of intent cards filling the grid budget.
let col_w = (right - left - gap) / 2.0;
let card_h = (grid_h - 2.0 * gap) / 3.0;
for (i, intent) in intents().iter().enumerate() {
let col = (i % 2) as f32;
let row = (i / 2) as f32;
let cx = left + col * (col_w + gap);
let cy = content_top + row * (card_h + gap);
let card = egui::Rect::from_min_size(egui::pos2(cx, cy), egui::vec2(col_w, card_h));
let resp = ui.interact(card, ui.id().with(("mobile_intent", i)), egui::Sense::click());
let p = ui.painter();
p.rect_filled(card, 12.0, if resp.hovered() { C_CARD_HOT } else { C_CARD });
p.rect_stroke(card, 12.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside);
p.text(
egui::pos2(card.center().x, card.top() + card.height() * 0.40),
egui::Align2::CENTER_CENTER,
intent.icon,
icons::font(30.0),
intent.accent,
);
p.text(
egui::pos2(card.center().x, card.bottom() - 18.0),
egui::Align2::CENTER_CENTER,
intent.label,
egui::FontId::proportional(15.0),
C_BRIGHT,
);
if resp.clicked() {
app.create_new_project_with_focus(intent.focus);
app.mobile_state.window_top = intent.window.0;
app.mobile_state.window_count = intent.window.1;
app.mobile_state.weights = [1.0, 1.0, 1.0];
}
}
// Recent projects list in the bottom third.
let recent_top = content_top + grid_h + 14.0;
ui.painter().text(
egui::pos2(left, recent_top),
egui::Align2::LEFT_TOP,
"Recent",
egui::FontId::proportional(13.0),
C_DIM,
);
let list_top = recent_top + 22.0;
let recents = app.config.get_recent_files();
if recents.is_empty() {
ui.painter().text(
egui::pos2(left, list_top + 8.0),
egui::Align2::LEFT_TOP,
"No recent projects",
egui::FontId::proportional(12.0),
C_DIM,
);
} else {
let row_h = 38.0;
let row_gap = 6.0;
let avail = rect.bottom() - margin - list_top;
let max_rows = ((avail + row_gap) / (row_h + row_gap)).floor().max(0.0) as usize;
let mut chosen: Option<std::path::PathBuf> = None;
for (j, path) in recents.iter().take(max_rows).enumerate() {
let ry = list_top + j as f32 * (row_h + row_gap);
let row_rect =
egui::Rect::from_min_max(egui::pos2(left, ry), egui::pos2(right, ry + row_h));
let resp =
ui.interact(row_rect, ui.id().with(("mobile_recent", j)), egui::Sense::click());
let p = ui.painter();
p.rect_filled(row_rect, 8.0, if resp.hovered() { C_CARD_HOT } else { C_CARD });
p.rect_stroke(row_rect, 8.0, egui::Stroke::new(1.0, C_LINE), egui::StrokeKind::Inside);
p.text(
egui::pos2(row_rect.left() + 12.0, row_rect.center().y),
egui::Align2::LEFT_CENTER,
icons::FOLDER_OPEN,
icons::font(15.0),
C_DIM,
);
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "Untitled".to_string());
p.text(
egui::pos2(row_rect.left() + 36.0, row_rect.center().y),
egui::Align2::LEFT_CENTER,
&name,
egui::FontId::proportional(13.0),
C_BRIGHT,
);
if resp.clicked() {
chosen = Some(path.clone());
}
}
if let Some(path) = chosen {
app.load_from_file(path);
app.app_mode = crate::AppMode::Editor;
}
}
});
}