172 lines
7.6 KiB
Rust
172 lines
7.6 KiB
Rust
//! 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;
|
||
}
|
||
}
|
||
});
|
||
}
|