Files
Unlucky-Launcher/crates/app/src/router.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

185 lines
5.0 KiB
Rust

//! 页面路由与 AI 助手栏形态(09 §1 / D16)。
//!
//! 栈语义抄 HMCL 的导航栈:**顶层页面互相替换**(实例/下载/日志/设置),
//! 子页面(实例详情、AI 独立页)压栈,`back()` 逐层回退。
//! 本模块刻意不依赖 Slint 类型,便于单元测试;UI 侧枚举映射在 main.rs 的转换层做。
use crate::dto::InstanceId;
/// 一级导航项(左侧导航栏高亮单位)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavSection {
/// 实例
Instances,
/// 下载
Download,
/// 日志
Logs,
/// 设置
Settings,
}
/// 页面路由。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Route {
/// G1 实例页(主页)
Instances,
/// G2 下载页
Download,
/// G3 日志页
Logs,
/// G6 设置页
Settings,
/// 实例详情(G4):从磁贴点进来,返回时回到实例页
Detail(InstanceId),
/// AI 助手独立页(D16 双形态之一)
Agent,
}
impl Route {
/// 是否是一级导航页(互相替换,不进栈)。
pub fn is_top_level(&self) -> bool {
matches!(
self,
Self::Instances | Self::Download | Self::Logs | Self::Settings
)
}
/// 该路由高亮哪个导航项(None = 没有导航项高亮,如 AI 独立页)。
pub fn nav(&self) -> Option<NavSection> {
match self {
Self::Instances | Self::Detail(_) => Some(NavSection::Instances),
Self::Download => Some(NavSection::Download),
Self::Logs => Some(NavSection::Logs),
Self::Settings => Some(NavSection::Settings),
Self::Agent => None,
}
}
}
/// AI 栏形态:右侧栏 / 独立全宽页(两形态共享同一份会话状态)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AiPanelMode {
/// 右侧常驻栏
Docked,
/// 拆出的独立全宽页
Page,
}
/// 导航栈。
#[derive(Debug, Clone)]
pub struct Router {
stack: Vec<Route>,
}
impl Default for Router {
fn default() -> Self {
Self::new()
}
}
impl Router {
/// 启动时停在实例页。
pub fn new() -> Self {
Self {
stack: vec![Route::Instances],
}
}
/// 当前页面。
pub fn route(&self) -> &Route {
// 栈不变量:构造后永远非空(navigate/back 都不允许清空)
self.stack.last().expect("router stack is never empty")
}
/// 栈深度(1 表示没有可回退的子页面)。
pub fn depth(&self) -> usize {
self.stack.len()
}
/// 是否能回退。
pub fn can_go_back(&self) -> bool {
self.stack.len() > 1
}
/// 跳转到某路由:一级页替换整栈,子页面压栈(重复目标不重复压栈)。
pub fn navigate(&mut self, route: Route) {
if route.is_top_level() {
self.stack.clear();
self.stack.push(route);
return;
}
if self.stack.last() == Some(&route) {
return;
}
self.stack.push(route);
}
/// 回退一层;已在根页面时返回 `false`。
pub fn back(&mut self) -> bool {
if self.stack.len() <= 1 {
return false;
}
self.stack.pop();
true
}
/// 当前 AI 栏形态(栈顶是 Agent 页即为独立页形态)。
pub fn ai_mode(&self) -> AiPanelMode {
if self.route() == &Route::Agent {
AiPanelMode::Page
} else {
AiPanelMode::Docked
}
}
}
#[cfg(test)]
mod tests {
use super::{AiPanelMode, NavSection, Route, Router};
#[test]
fn top_level_navigation_replaces_the_stack() {
let mut router = Router::new();
router.navigate(Route::Detail("a".into()));
assert_eq!(router.depth(), 2);
router.navigate(Route::Download);
assert_eq!(router.depth(), 1);
assert_eq!(router.route(), &Route::Download);
}
#[test]
fn detail_pushes_and_back_pops_to_instances() {
let mut router = Router::new();
router.navigate(Route::Detail("survival".into()));
assert!(router.can_go_back());
assert_eq!(router.route().nav(), Some(NavSection::Instances));
assert!(router.back());
assert_eq!(router.route(), &Route::Instances);
assert!(!router.back());
}
#[test]
fn agent_page_switches_ai_panel_mode() {
let mut router = Router::new();
assert_eq!(router.ai_mode(), AiPanelMode::Docked);
router.navigate(Route::Agent);
assert_eq!(router.ai_mode(), AiPanelMode::Page);
router.back();
assert_eq!(router.ai_mode(), AiPanelMode::Docked);
}
#[test]
fn navigating_to_the_same_sub_route_does_not_grow_the_stack() {
let mut router = Router::new();
router.navigate(Route::Agent);
router.navigate(Route::Agent);
assert_eq!(router.depth(), 2);
assert_eq!(router.route().nav(), None);
}
}