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
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
185 lines
5.0 KiB
Rust
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);
|
|
}
|
|
}
|