Unlucky Launcher v0.0.1-alpha
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
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 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,184 @@
|
||||
//! 页面路由与 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user