Skip to content

Commit f8dca1b

Browse files
AndrewAltimitAI Agent BotclaudeAI Review Agent
authored
feat: PSP lightweight skin presets and SDI-based views (#55)
* feat: PSP lightweight skin presets and SDI-based view rendering Add runtime skin switching for PSP without the ~850KB TOML parser overhead. Nine presets (Psix, Classic, Cyberpunk, Retro CGA, Solarized, High Contrast, Terminal, Altimit, Tactical) store only 9 base colors each and derive full ActiveTheme via the new `from_base_colors()` method. Skin selection persists to config.rcfg and cycles via `skin` terminal command. Migrate 6 classic views (File Manager, Photo Viewer, Music Player, Browser, Radio, TV Guide) from direct rendering to SDI scene graph objects, enabling theme-driven colors and consistent rendering with dashboard/terminal. Playback overlays (visualizer, video) remain direct-rendered for per-frame animation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address AI review feedback (iteration 1) Automated fix by Claude in response to Gemini/Codex review. Iteration: 1/5 Co-Authored-By: AI Review Agent <noreply@anthropic.com> * fix: broken doc link and per-frame text allocations in SDI views Fix unresolved `[from_skin]` rustdoc link → `[Self::from_skin]` that caused the CI doc build to fail with -D warnings. Add `set_text()` helper to skip heap allocation when SDI object text hasn't changed between frames (addresses Gemini review iteration 2 feedback that the review-response agent claimed to fix but didn't). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: set_text takes &str to avoid eager heap allocations per frame Change set_text(slot, value: String) → set_text(slot, value: &str) so callers that already have &str (title, icon_text, name_text, status_msg, etc.) no longer allocate a String just for comparison. Only allocates when the text actually changed. Addresses Gemini review iteration 3 feedback (the review-response agent claimed to fix this but didn't). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: AI Agent Bot <ai-agent@localhost> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: AI Review Agent <ai-review-agent@localhost>
1 parent adb2479 commit f8dca1b

File tree

6 files changed

+1777
-49
lines changed

6 files changed

+1777
-49
lines changed

crates/oasis-backend-psp/src/commands.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ pub fn execute_command(cmd: &str, config: &mut psp::config::Config) -> CommandRe
9494
String::from(" plugin install - Install overlay PRX"),
9595
String::from(" plugin remove - Remove overlay PRX"),
9696
String::from(" plugin status - Plugin load status"),
97+
String::from(" skin - List available skins"),
98+
String::from(" skin NAME - Switch to named skin"),
9799
String::from(" selftest - Run built-in test suite"),
98100
String::from(" clear - Clear terminal"),
99101
String::new(),

crates/oasis-backend-psp/src/main.rs

Lines changed: 102 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,21 @@ use oasis_backend_psp::{
2020
};
2121

2222
// oasis-core SDI integration types.
23-
use oasis_core::active_theme::ActiveTheme;
2423
use oasis_core::bottombar::BottomBar;
2524
use oasis_core::dashboard::{AppEntry as CoreAppEntry, DashboardConfig, DashboardState};
2625
use oasis_core::platform::{BatteryState, CpuClock, PowerInfo, SystemTime};
27-
use oasis_core::skin::SkinFeatures;
2826
use oasis_core::statusbar::StatusBar;
2927
use oasis_core::terminal_sdi;
3028

3129
mod boot;
3230
mod chrome;
3331
mod commands;
3432
mod desktop;
33+
mod skins;
3534
mod theme;
3635
mod types;
3736
mod views;
37+
mod views_sdi;
3838

3939
use theme::*;
4040
use types::*;
@@ -155,32 +155,13 @@ fn psp_main() {
155155
dbg_log("[EBOOT] sysinfo queried");
156156

157157
// -- ActiveTheme (SDI integration) --
158-
// Use default theme directly to avoid pulling in the TOML parser and
159-
// 17 embedded skin files (~850KB code). ActiveTheme::default() provides
160-
// PSIX-style layout already matched to 480x272.
158+
// Derive theme from a lightweight preset (9 base colors) instead of
159+
// pulling in the TOML parser + 18 embedded skin strings (~850KB).
161160
dbg_log("[EBOOT] creating theme...");
162-
let mut active_theme = ActiveTheme::default()
163-
.with_screen_size(SCREEN_WIDTH, SCREEN_HEIGHT);
164-
// PSP: make bar backgrounds opaque to prevent darkening window content.
165-
// Default is semi-transparent (alpha=80) which looks muddy on 480x272.
166-
active_theme.bar.statusbar_bg = Color::rgba(10, 10, 20, 255);
167-
active_theme.bar.bg = Color::rgba(10, 10, 20, 255);
168-
// PSP: match actual bar sizes (theme.rs constants) so SDI grid aligns.
169-
active_theme.statusbar_height = 18;
170-
active_theme.tab_row_height = 0;
171-
active_theme.bottombar_height = 32;
172-
// PSP: compact icons for 4x3 grid on 480x272.
173-
active_theme.icon_width = 34;
174-
active_theme.icon_height = 34;
175-
active_theme.icon_stripe_h = 6;
176-
active_theme.icon_fold_size = 5;
177-
active_theme.grid_padding_x = 8;
178-
active_theme.grid_padding_y = 2;
179-
active_theme.cursor_pad = 2;
180-
let mut skin_features = SkinFeatures::default();
181-
skin_features.grid_cols = 4;
182-
skin_features.grid_rows = 3;
183-
skin_features.icons_per_page = 12;
161+
let skin_key = config.get_str("skin").unwrap_or("psix");
162+
let mut current_preset = skins::PspSkinPreset::from_key(skin_key);
163+
let mut active_theme = current_preset.to_active_theme();
164+
let skin_features = skins::PspSkinPreset::skin_features();
184165
dbg_log("[EBOOT] active_theme created");
185166
let dash_config = DashboardConfig::from_features(&skin_features, &active_theme);
186167

@@ -228,6 +209,7 @@ fn psp_main() {
228209
// -- App mode --
229210
let mut app_mode = AppMode::Classic;
230211
let mut classic_view = ClassicView::Dashboard;
212+
let mut prev_classic_view = ClassicView::Dashboard;
231213

232214
let mut icons_hidden: bool = false;
233215
let mut viz_frame: u32 = 0;
@@ -896,6 +878,47 @@ fn psp_main() {
896878
audio.send(AudioCmd::Stop);
897879
(vec!["Stopped.".into()], false)
898880
},
881+
"skin" => {
882+
let names: Vec<String> = skins::PspSkinPreset::ALL
883+
.iter()
884+
.map(|p| {
885+
let marker =
886+
if *p == current_preset { ">" } else { " " };
887+
format!("{} {}", marker, p.name())
888+
})
889+
.collect();
890+
let mut out = vec!["Skins (use 'skin NAME'):".into()];
891+
out.extend(names);
892+
(out, false)
893+
},
894+
_ if cmd.trim().starts_with("skin ") => {
895+
let key = cmd.trim().strip_prefix("skin ").unwrap().trim();
896+
let preset = skins::PspSkinPreset::from_key(key);
897+
if preset == current_preset {
898+
(vec![format!("Already using '{}'.", key)], false)
899+
} else {
900+
current_preset = preset;
901+
active_theme = preset.to_active_theme();
902+
dashboard.config = DashboardConfig::from_features(
903+
&skin_features,
904+
&active_theme,
905+
);
906+
config.set(
907+
"skin",
908+
psp::config::ConfigValue::Str(
909+
preset.key().to_string(),
910+
),
911+
);
912+
let _ = config.save(CONFIG_PATH);
913+
(
914+
vec![format!(
915+
"Skin changed to '{}'.",
916+
preset.name()
917+
)],
918+
false,
919+
)
920+
}
921+
},
899922
_ => {
900923
let r = commands::execute_command(&cmd, &mut config);
901924
(r.lines, r.used_dialog)
@@ -1628,6 +1651,13 @@ fn psp_main() {
16281651
terminal_sdi::set_terminal_visible(&mut sdi, false);
16291652
}
16301653

1654+
// View transition: hide old SDI objects, set up new ones.
1655+
if classic_view != prev_classic_view {
1656+
views_sdi::hide_all(&mut sdi);
1657+
views_sdi::setup_view(&mut sdi, classic_view);
1658+
prev_classic_view = classic_view;
1659+
}
1660+
16311661
match classic_view {
16321662
ClassicView::Dashboard => {
16331663
backend.force_bitmap_font = true;
@@ -1666,9 +1696,8 @@ fn psp_main() {
16661696
backend.force_bitmap_font = false;
16671697
},
16681698
ClassicView::FileManager => {
1669-
backend.force_bitmap_font = true;
1670-
views::draw_file_manager_dual(
1671-
&mut backend,
1699+
views_sdi::update_file_manager(
1700+
&mut sdi,
16721701
&fm_path,
16731702
&fm_entries,
16741703
fm_selected,
@@ -1678,38 +1707,44 @@ fn psp_main() {
16781707
fm2_selected,
16791708
fm2_scroll,
16801709
fm_active_panel,
1710+
&active_theme,
16811711
);
1712+
backend.force_bitmap_font = true;
16821713
chrome::draw_button_hints(
16831714
&mut backend,
16841715
&[("X", "Open"), ("O", "Back"), ("<>", "Panel"), ("^v", "Nav")],
16851716
);
16861717
backend.force_bitmap_font = false;
16871718
},
16881719
ClassicView::PhotoViewer => {
1689-
backend.force_bitmap_font = true;
16901720
if pv_viewing {
1691-
views::draw_photo_view(&mut backend, pv_tex, pv_img_w, pv_img_h);
1721+
views_sdi::update_photo_view(&mut sdi, pv_tex, pv_img_w, pv_img_h);
1722+
backend.force_bitmap_font = true;
16921723
chrome::draw_button_hints(&mut backend, &[("O", "Back")]);
1724+
backend.force_bitmap_font = false;
16931725
} else if pv_loading {
16941726
desktop::draw_loading_indicator(&mut backend, "Decoding image...");
16951727
} else {
1696-
views::draw_photo_browser(
1697-
&mut backend,
1728+
views_sdi::update_photo_browser(
1729+
&mut sdi,
16981730
&pv_path,
16991731
&pv_entries,
17001732
pv_selected,
17011733
pv_scroll,
1734+
&active_theme,
17021735
);
1736+
backend.force_bitmap_font = true;
17031737
chrome::draw_button_hints(
17041738
&mut backend,
17051739
&[("X", "View"), ("O", "Back"), ("^v", "Nav")],
17061740
);
1741+
backend.force_bitmap_font = false;
17071742
}
1708-
backend.force_bitmap_font = false;
17091743
},
17101744
ClassicView::MusicPlayer => {
1711-
backend.force_bitmap_font = true;
17121745
if audio.is_playing() {
1746+
// Now-playing view uses direct rendering for visualizer.
1747+
backend.force_bitmap_font = true;
17131748
views::draw_music_player_threaded(
17141749
&mut backend,
17151750
&mp_file_name,
@@ -1720,34 +1755,38 @@ fn psp_main() {
17201755
&mut backend,
17211756
&[("X", "Pause"), ("[]", "Stop"), ("^v", "Back")],
17221757
);
1758+
backend.force_bitmap_font = false;
17231759
} else {
1724-
views::draw_music_browser(
1725-
&mut backend,
1760+
views_sdi::update_music_browser(
1761+
&mut sdi,
17261762
&mp_path,
17271763
&mp_entries,
17281764
mp_selected,
17291765
mp_scroll,
1766+
&active_theme,
17301767
);
1768+
backend.force_bitmap_font = true;
17311769
chrome::draw_button_hints(
17321770
&mut backend,
17331771
&[("X", "Play"), ("O", "Back"), ("^v", "Nav")],
17341772
);
1773+
backend.force_bitmap_font = false;
17351774
}
1736-
backend.force_bitmap_font = false;
17371775
},
17381776
ClassicView::Browser => {
1739-
backend.force_bitmap_font = true;
17401777
if br_loading {
17411778
desktop::draw_loading_indicator(&mut backend, "Loading page...");
17421779
} else {
1743-
views::draw_browser_view(
1744-
&mut backend,
1780+
views_sdi::update_browser(
1781+
&mut sdi,
17451782
&br_url,
17461783
&br_content_lines,
17471784
br_scroll,
17481785
&br_status_msg,
1786+
&active_theme,
17491787
);
17501788
}
1789+
backend.force_bitmap_font = true;
17511790
chrome::draw_button_hints(
17521791
&mut backend,
17531792
&[
@@ -1763,7 +1802,12 @@ fn psp_main() {
17631802
backend.force_bitmap_font = true;
17641803
match radio_status {
17651804
RadioStatus::Stopped => {
1766-
views::draw_radio_stations(&mut backend, radio_selected, radio_scroll);
1805+
views_sdi::update_radio(
1806+
&mut sdi,
1807+
radio_selected,
1808+
radio_scroll,
1809+
&active_theme,
1810+
);
17671811
chrome::draw_button_hints(
17681812
&mut backend,
17691813
&[("X", "Tune"), ("^v", "Nav"), ("O", "Back")],
@@ -1818,12 +1862,13 @@ fn psp_main() {
18181862
&[("X", "Retry"), ("O", "Back")],
18191863
);
18201864
} else {
1821-
views::draw_tv_channels(
1822-
&mut backend,
1865+
views_sdi::update_tv_channels(
1866+
&mut sdi,
18231867
&tv_channels,
18241868
&tv_catalogs,
18251869
tv_selected,
18261870
tv_scroll,
1871+
&active_theme,
18271872
);
18281873
chrome::draw_button_hints(
18291874
&mut backend,
@@ -1984,10 +2029,18 @@ fn psp_main() {
19842029
// - Overlay layer always (status bar, bottom bar at z=900)
19852030
// This avoids 100+ unnecessary draw calls in non-dashboard views.
19862031
let needs_base = match app_mode {
1987-
AppMode::Classic => matches!(
1988-
classic_view,
1989-
ClassicView::Dashboard | ClassicView::Terminal
1990-
),
2032+
AppMode::Classic => {
2033+
// SDI base layer is needed for dashboard, terminal, and all
2034+
// SDI-migrated list views. Skip only when a direct-rendered
2035+
// overlay is active (music now-playing, radio playing, etc.).
2036+
let is_direct_only =
2037+
(classic_view == ClassicView::MusicPlayer && audio.is_playing())
2038+
|| (classic_view == ClassicView::Radio
2039+
&& radio_status != RadioStatus::Stopped)
2040+
|| (classic_view == ClassicView::TvGuide
2041+
&& (tv_tuned.is_some() || !tv_error_msg.is_empty()));
2042+
!is_direct_only
2043+
},
19912044
AppMode::Desktop => false, // WM draws windows directly
19922045
};
19932046
if needs_base {

0 commit comments

Comments
 (0)