fix(desktop): show update check status
This commit is contained in:
parent
9a09d3cdbe
commit
76af87930f
|
|
@ -24,14 +24,53 @@ fn system_auto_update_switch_has_click_target() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn system_tab_uses_ts_compact_auto_update_card_height() {
|
||||
fn system_tab_reserves_space_for_update_status() {
|
||||
let mut state = EditorState::default();
|
||||
state.editor_ui.agent_settings.tab = AgentSettingsTab::System;
|
||||
let panel = AgentSettingsPanel::for_editor(&state);
|
||||
|
||||
assert_eq!(
|
||||
panel.content_total_height(),
|
||||
320.0,
|
||||
"System tab = title + auto-update + experimental + pencil-cursor picker"
|
||||
378.0,
|
||||
"System tab = title + update status + experimental + pencil-cursor picker"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_tab_paints_each_update_probe_result() {
|
||||
let cases = [
|
||||
(op_editor_core::UpdateStatus::Idle, "Not checked yet"),
|
||||
(op_editor_core::UpdateStatus::Checking, "Checking…"),
|
||||
(op_editor_core::UpdateStatus::UpToDate, "Up to date"),
|
||||
(
|
||||
op_editor_core::UpdateStatus::Available {
|
||||
version: "9.8.7".to_string(),
|
||||
},
|
||||
"Update available v9.8.7",
|
||||
),
|
||||
(op_editor_core::UpdateStatus::Error, "Check failed"),
|
||||
];
|
||||
|
||||
for (status, expected) in cases {
|
||||
let mut state = EditorState::default();
|
||||
state.editor_ui.locale = op_editor_core::Locale::EnUs;
|
||||
state.editor_ui.agent_settings.tab = AgentSettingsTab::System;
|
||||
state.editor_ui.update_status = status;
|
||||
let panel = AgentSettingsPanel::for_editor(&state);
|
||||
let rect = panel.rect(1200.0, 800.0);
|
||||
let mut backend = CaptureBackend::default();
|
||||
let mut cx = PaintCx {
|
||||
backend: &mut backend,
|
||||
};
|
||||
|
||||
panel.paint(&mut cx, rect);
|
||||
|
||||
assert!(
|
||||
backend
|
||||
.text_effective_points
|
||||
.iter()
|
||||
.any(|(text, _)| text == expected),
|
||||
"System tab should paint update status {expected:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! System tab of the settings modal.
|
||||
//!
|
||||
//! Renders the auto-update preference row plus the experimental-features
|
||||
//! opt-in (gates canvas Preview mode + the property-panel Widget section).
|
||||
//! Renders the auto-update preference and live release-probe status plus the
|
||||
//! experimental-features opt-in and pencil-cursor picker.
|
||||
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::agent_settings_i18n::t as t_settings;
|
||||
|
|
@ -10,14 +10,16 @@ use crate::widgets::agent_settings_switch::{
|
|||
};
|
||||
use crate::widgets::button::tokens_from_theme;
|
||||
use crate::widgets::PaintCx;
|
||||
use crate::{Point2D, Rect, TextLayout};
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
use jian_widgets::components::card::Card;
|
||||
use op_editor_core::agent_settings::AgentSettings;
|
||||
use op_editor_core::editor_ui_state::EditorUiState;
|
||||
use op_editor_core::editor_ui_state::{EditorUiState, UpdateStatus};
|
||||
|
||||
const TITLE_H: f32 = 36.0;
|
||||
const CARD_H: f32 = 58.0;
|
||||
const TOGGLE_CARD_H: f32 = 58.0;
|
||||
const UPDATE_CARD_H: f32 = 116.0;
|
||||
const CARD_GAP: f32 = 12.0;
|
||||
const STATUS_DOT_RADIUS: f32 = 4.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SystemHit {
|
||||
|
|
@ -34,13 +36,18 @@ const CURSOR_SWATCH: f32 = 52.0;
|
|||
const CURSOR_SWATCH_GAP: f32 = 10.0;
|
||||
|
||||
pub(super) fn content_height() -> f32 {
|
||||
12.0 + TITLE_H + CARD_H + CARD_GAP + CARD_H + CARD_GAP + CURSOR_CARD_H + 24.0
|
||||
let update = if AUTO_UPDATE_AVAILABLE {
|
||||
UPDATE_CARD_H + CARD_GAP
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
12.0 + TITLE_H + update + TOGGLE_CARD_H + CARD_GAP + CURSOR_CARD_H + 24.0
|
||||
}
|
||||
|
||||
fn cursor_card_rect(content: Rect) -> Rect {
|
||||
let prev = experimental_card_rect(content);
|
||||
Rect {
|
||||
origin: Point2D::new(content.origin.x, prev.origin.y + CARD_H + CARD_GAP),
|
||||
origin: Point2D::new(content.origin.x, prev.origin.y + TOGGLE_CARD_H + CARD_GAP),
|
||||
size: Point2D::new(content.size.x, CURSOR_CARD_H),
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +67,7 @@ fn cursor_swatch_rect(card: Rect, i: usize) -> Rect {
|
|||
fn auto_update_card_rect(content: Rect) -> Rect {
|
||||
Rect {
|
||||
origin: Point2D::new(content.origin.x, content.origin.y + 12.0 + TITLE_H),
|
||||
size: Point2D::new(content.size.x, CARD_H),
|
||||
size: Point2D::new(content.size.x, UPDATE_CARD_H),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +86,9 @@ fn experimental_card_rect(content: Rect) -> Rect {
|
|||
Rect {
|
||||
origin: Point2D::new(
|
||||
content.origin.x,
|
||||
content.origin.y + 12.0 + TITLE_H + CARD_H + CARD_GAP,
|
||||
content.origin.y + 12.0 + TITLE_H + UPDATE_CARD_H + CARD_GAP,
|
||||
),
|
||||
size: Point2D::new(content.size.x, CARD_H),
|
||||
size: Point2D::new(content.size.x, TOGGLE_CARD_H),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +98,7 @@ fn switch_rect_for(card: Rect) -> Rect {
|
|||
Rect {
|
||||
origin: Point2D::new(
|
||||
card.origin.x + card.size.x - 16.0 - SETTINGS_SWITCH_W,
|
||||
card.origin.y + (CARD_H - SETTINGS_SWITCH_H) / 2.0,
|
||||
card.origin.y + (TOGGLE_CARD_H - SETTINGS_SWITCH_H) / 2.0,
|
||||
),
|
||||
size: Point2D::new(SETTINGS_SWITCH_W, SETTINGS_SWITCH_H),
|
||||
}
|
||||
|
|
@ -143,6 +150,7 @@ pub(super) fn paint_system_tab(
|
|||
"settings.autoUpdateDesc",
|
||||
settings.auto_update_enabled,
|
||||
);
|
||||
paint_update_status(cx, theme, ui, auto_update_card_rect(content));
|
||||
}
|
||||
paint_toggle_card(
|
||||
cx,
|
||||
|
|
@ -156,6 +164,108 @@ pub(super) fn paint_system_tab(
|
|||
paint_cursor_card(cx, theme, ui, cursor_card_rect(content));
|
||||
}
|
||||
|
||||
/// Lower half of the auto-update card: current probe state and build version.
|
||||
fn paint_update_status(cx: &mut PaintCx<'_>, theme: &Theme, ui: &EditorUiState, card: Rect) {
|
||||
let (color, status_key, description_key) = status_view(theme, &ui.update_status);
|
||||
let divider_y = card.origin.y + TOGGLE_CARD_H;
|
||||
cx.backend.stroke_line(
|
||||
Point2D::new(card.origin.x + 16.0, divider_y),
|
||||
Point2D::new(card.origin.x + card.size.x - 16.0, divider_y),
|
||||
theme.border,
|
||||
1.0,
|
||||
);
|
||||
|
||||
let dot_center_y = divider_y + 20.0;
|
||||
cx.backend.fill_oval(
|
||||
Rect {
|
||||
origin: Point2D::new(card.origin.x + 16.0, dot_center_y - STATUS_DOT_RADIUS),
|
||||
size: Point2D::new(STATUS_DOT_RADIUS * 2.0, STATUS_DOT_RADIUS * 2.0),
|
||||
},
|
||||
color,
|
||||
);
|
||||
|
||||
let status_text = match &ui.update_status {
|
||||
UpdateStatus::Available { version } => {
|
||||
format!("{} v{version}", t_settings(ui, status_key))
|
||||
}
|
||||
_ => t_settings(ui, status_key).to_string(),
|
||||
};
|
||||
let status = TextLayout::single_run(
|
||||
&status_text,
|
||||
"system-ui",
|
||||
12.0,
|
||||
color.to_jian(),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&status,
|
||||
Point2D::new(card.origin.x + 32.0, divider_y + 24.0),
|
||||
);
|
||||
|
||||
let current_version = format!(
|
||||
"{}: v{}",
|
||||
t_settings(ui, "settings.system.currentVersion"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
let current_version_w = cx.backend.measure_text(¤t_version, 10.0);
|
||||
let version = TextLayout::single_run(
|
||||
¤t_version,
|
||||
"system-ui",
|
||||
10.0,
|
||||
theme.muted_foreground.to_jian(),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&version,
|
||||
Point2D::new(
|
||||
card.origin.x + card.size.x - 16.0 - current_version_w,
|
||||
divider_y + 24.0,
|
||||
),
|
||||
);
|
||||
|
||||
let description = TextLayout::single_run(
|
||||
t_settings(ui, description_key),
|
||||
"system-ui",
|
||||
11.0,
|
||||
theme.muted_foreground.to_jian(),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&description,
|
||||
Point2D::new(card.origin.x + 16.0, divider_y + 46.0),
|
||||
);
|
||||
}
|
||||
|
||||
fn status_view(theme: &Theme, status: &UpdateStatus) -> (Color, &'static str, &'static str) {
|
||||
match status {
|
||||
UpdateStatus::Idle => (
|
||||
theme.muted_foreground,
|
||||
"settings.system.idle",
|
||||
"settings.system.idleDescription",
|
||||
),
|
||||
UpdateStatus::Checking => (
|
||||
theme.primary,
|
||||
"settings.system.checking",
|
||||
"settings.system.checkingDescription",
|
||||
),
|
||||
UpdateStatus::UpToDate => (
|
||||
theme.status_success,
|
||||
"settings.system.upToDate",
|
||||
"settings.system.upToDateDescription",
|
||||
),
|
||||
UpdateStatus::Available { .. } => (
|
||||
theme.status_warning,
|
||||
"settings.system.updateAvailable",
|
||||
"settings.system.updateAvailableDescription",
|
||||
),
|
||||
UpdateStatus::Error => (
|
||||
theme.destructive,
|
||||
"settings.system.errorStatus",
|
||||
"settings.system.errorDescription",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The pencil-cursor picker: a labelled card with one swatch per style,
|
||||
/// each drawing its real silhouette at half scale; the active style gets
|
||||
/// an accent ring.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ impl DesktopApp {
|
|||
.auto_update_enabled,
|
||||
)
|
||||
};
|
||||
if update_probe.is_pending() {
|
||||
host.editor_state_mut().editor_ui.update_status =
|
||||
op_editor_core::UpdateStatus::Checking;
|
||||
}
|
||||
let model_probe = if cfg!(test) {
|
||||
op_host_services::model_discovery::ModelProbe::idle()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -244,20 +244,20 @@ impl DesktopApp {
|
|||
false
|
||||
}
|
||||
A::CheckUpdates => {
|
||||
// Re-run the probe; the System tab reflects `Checking`
|
||||
// immediately and the result lands on a later frame.
|
||||
// Skip when a probe is already in flight so repeated
|
||||
// menu clicks can't stack untracked worker threads.
|
||||
if self.update_probe.is_pending() {
|
||||
false
|
||||
} else {
|
||||
self.host.editor_state_mut().editor_ui.update_status =
|
||||
op_editor_core::UpdateStatus::Checking;
|
||||
self.host.mark_editor_state_dirty();
|
||||
// Make every manual check visible immediately. An automatic
|
||||
// startup probe may already be running, so reveal that same
|
||||
// worker instead of stacking another request.
|
||||
self.host.apply_open_agent_settings_tab(
|
||||
op_editor_core::agent_settings::AgentSettingsTab::System,
|
||||
);
|
||||
self.host.editor_state_mut().editor_ui.update_status =
|
||||
op_editor_core::UpdateStatus::Checking;
|
||||
self.host.mark_editor_state_dirty();
|
||||
if !self.update_probe.is_pending() {
|
||||
self.update_probe = update_check::UpdateProbe::spawn();
|
||||
self.update_prompt_shown = false;
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
A::OpenGithub => {
|
||||
update_check::open_url("https://github.com/ZSeven-W/openpencil");
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ impl UpdateProbe {
|
|||
Err(TryRecvError::Empty) => None,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.rx = None;
|
||||
None
|
||||
Some(UpdateStatus::Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -422,6 +422,16 @@ mod tests {
|
|||
|
||||
assert!(!probe.is_pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnected_worker_resolves_as_error_instead_of_staying_checking() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
drop(tx);
|
||||
let mut probe = UpdateProbe { rx: Some(rx) };
|
||||
|
||||
assert_eq!(probe.poll(), Some(UpdateStatus::Error));
|
||||
assert!(!probe.is_pending());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
use super::WidgetHostNative;
|
||||
use op_editor_core::{
|
||||
agent_settings::SettingsFocus, figma_import_state::ImportSource, ui_draft::ColorTarget, NodeId,
|
||||
PropertyFocus,
|
||||
agent_settings::SettingsFocus, figma_import_state::ImportSource, ui_draft::ColorTarget,
|
||||
AgentSettingsTab, NodeId, PropertyFocus,
|
||||
};
|
||||
|
||||
fn seed(host: &mut WidgetHostNative, json: &str) {
|
||||
|
|
@ -101,6 +101,33 @@ fn closing_settings_clears_focus_before_an_import_shortcut() {
|
|||
assert!(host.editor_state().editor_ui.figma_import_open);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_a_specific_settings_tab_reveals_it_and_commits_prior_input() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
{
|
||||
let ui = &mut host.editor_state_mut().editor_ui;
|
||||
ui.agent_settings.tab = AgentSettingsTab::Mcp;
|
||||
ui.agent_settings.scroll_y.offset = 84.0;
|
||||
ui.agent_settings.focus = Some(SettingsFocus::McpPort);
|
||||
ui.settings_input.set_text("4321");
|
||||
ui.design_md_panel.open = true;
|
||||
ui.component_browser_open = true;
|
||||
ui.open_icon_picker(false);
|
||||
}
|
||||
|
||||
assert!(host.apply_open_agent_settings_tab(AgentSettingsTab::System));
|
||||
|
||||
let ui = &host.editor_state().editor_ui;
|
||||
assert!(ui.agent_settings_open);
|
||||
assert_eq!(ui.agent_settings.tab, AgentSettingsTab::System);
|
||||
assert_eq!(ui.agent_settings.scroll_y.offset, 0.0);
|
||||
assert!(ui.agent_settings.focus.is_none());
|
||||
assert_eq!(ui.agent_settings.mcp_server.port, 4321);
|
||||
assert!(!ui.design_md_panel.open);
|
||||
assert!(!ui.component_browser_open);
|
||||
assert!(!ui.icon_picker.open);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_shortcut_blurs_covered_text_and_ime_owners() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use super::WidgetHostNative;
|
||||
use op_editor_core::host_keyboard_transitions as shared;
|
||||
use op_editor_core::{figma_import_state::ImportSource, ReorderDirection};
|
||||
use op_editor_core::{figma_import_state::ImportSource, AgentSettingsTab, ReorderDirection};
|
||||
|
||||
impl WidgetHostNative {
|
||||
/// True when a non-chat text surface owns keyboard input. Plain-string
|
||||
|
|
@ -541,6 +541,42 @@ impl WidgetHostNative {
|
|||
true
|
||||
}
|
||||
|
||||
/// Open the floating settings modal on a specific tab.
|
||||
///
|
||||
/// Native menu commands use this instead of toggling so a command can
|
||||
/// reveal its live status even when Settings is already open elsewhere.
|
||||
pub fn apply_open_agent_settings_tab(&mut self, tab: AgentSettingsTab) -> bool {
|
||||
self.commit_variable_row_focus_if_any();
|
||||
self.editor_state.editor_ui.blur_collab_join_input();
|
||||
self.commit_settings_focus_if_any();
|
||||
shared::commit_editing_for_modal(&mut self.editor_state);
|
||||
self.design_md_drag = None;
|
||||
self.component_browser_drag = None;
|
||||
self.icon_picker_drag = None;
|
||||
{
|
||||
let ui = &mut self.editor_state.editor_ui;
|
||||
ui.close_font_picker();
|
||||
ui.close_icon_picker();
|
||||
ui.design_md_panel.open = false;
|
||||
ui.design_md_panel.hover = None;
|
||||
ui.component_browser_open = false;
|
||||
ui.component_browser_kit_picker_open = false;
|
||||
ui.component_browser_confirm_delete_kit = None;
|
||||
ui.component_browser_hover = None;
|
||||
ui.layer_context_menu = None;
|
||||
ui.agent_settings_open = true;
|
||||
ui.agent_settings.tab = tab;
|
||||
ui.agent_settings.scroll_y.offset = 0.0;
|
||||
ui.agent_settings_drag = None;
|
||||
ui.ime_preedit = None;
|
||||
}
|
||||
self.editor_state.ui.path_anchor_menu = None;
|
||||
self.close_image_popovers_for_higher_overlay();
|
||||
self.editor_state.chat.blur_input(self.now_ms);
|
||||
self.mark_dirty();
|
||||
true
|
||||
}
|
||||
|
||||
/// Single-key tool switch (V / R / O / L / T / F / P / H). Also
|
||||
/// DISCARDS any in-flight pen path (TS `onToolChange` resets the
|
||||
/// pen preview without committing, `skia-pen-tool.ts:38-50`).
|
||||
|
|
|
|||
Loading…
Reference in a new issue