Add mobile new-file intent picker (P2)
When LB_MOBILE_UI is set, the desktop start screen is replaced by a phone intent picker: a 2x3 grid of icon cards (Draw / Animate / Compose / Record / Edit video / Blank) taking the top ~2/3, and a scrollable Recent-projects list in the bottom ~1/3. Each intent reuses EditorApp::create_new_project_with_focus and then sets the initial stack window (e.g. Draw -> fullscreen Stage, Compose -> Timeline + Piano Roll + Keys). Recent rows load the project and enter the editor. Cards are icon-only (Lucide glyph + label) for legibility on a phone. Adds the intent icons to the Lucide set.
This commit is contained in:
parent
b25e639a1c
commit
f2cb516e8a
|
|
@ -6533,7 +6533,11 @@ impl eframe::App for EditorApp {
|
|||
|
||||
// Render start screen or editor based on app mode
|
||||
if self.app_mode == AppMode::StartScreen {
|
||||
if self.mobile_active() {
|
||||
mobile::intent::render(self, ctx);
|
||||
} else {
|
||||
self.render_start_screen(ctx);
|
||||
}
|
||||
return; // Skip editor rendering
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,3 +45,11 @@ pub const SEARCH: &str = "\u{e151}";
|
|||
pub const PLUS: &str = "\u{e13d}";
|
||||
pub const X: &str = "\u{e1b2}";
|
||||
pub const MENU: &str = "\u{e115}";
|
||||
// Intent-picker icons.
|
||||
pub const BRUSH: &str = "\u{e1d3}";
|
||||
pub const FILM: &str = "\u{e0d0}";
|
||||
pub const MUSIC: &str = "\u{e122}";
|
||||
pub const MIC: &str = "\u{e118}";
|
||||
pub const CLAPPERBOARD: &str = "\u{e29b}";
|
||||
pub const SQUARE_DASHED: &str = "\u{e1cb}";
|
||||
pub const FOLDER_OPEN: &str = "\u{e247}";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
//! 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ use crate::panes::NodePath;
|
|||
use crate::RenderContext;
|
||||
|
||||
pub mod icons;
|
||||
pub mod intent;
|
||||
mod stack;
|
||||
mod surface;
|
||||
mod transport;
|
||||
|
|
|
|||
Loading…
Reference in New Issue