From d3d320877e7a9987322971bd66bb771803208dad Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 10:59:09 +0300 Subject: [PATCH 01/52] feat(B42): TikZ-native studio tool rail, responsive panel contract and persisted workspace preferences --- crates/tikzarium-app/src/studio_ui.rs | 236 ++++++++++++++++++++++++++ crates/tikzarium-app/src/ui_theme.rs | 22 ++- 2 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 crates/tikzarium-app/src/studio_ui.rs diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs new file mode 100644 index 00000000..898ac29d --- /dev/null +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -0,0 +1,236 @@ +//! B42 studio workbench: native TikZ tool rail and context-sensitive docks. +//! The C catalogue remains the single source of tool/help facts, and the +//! external XeLaTeX PDF remains authoritative. No internal TeX renderer. +use eframe::egui; +use serde::{Deserialize, Serialize}; +use tikzarium_extensions::primitives::Evidence; + +use crate::tikz_authoring::{self, Session, TOOLS, Tool}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum StudioDock { + #[default] + Properties, + Libraries, + Help, +} + +impl StudioDock { + pub const ALL: [Self; 3] = [Self::Properties, Self::Libraries, Self::Help]; + + pub const fn label(self) -> &'static str { + match self { + Self::Properties => "Свойства", + Self::Libraries => "Библиотеки", + Self::Help => "Справка", + } + } +} + +/// Layout policy uses logical egui points, not PDF coordinates. At narrow +/// widths the rail becomes icon-only, and the dock can be hidden explicitly. +#[derive(Clone, Copy, Debug)] +pub struct StudioLayout { + pub tool_rail_width: f32, + pub inspector_width: f32, + pub inspector_max: f32, + pub narrow: bool, +} + +impl StudioLayout { + #[must_use] + pub fn for_width(width: f32) -> Self { + let width = if width.is_finite() { width.max(0.0) } else { 0.0 }; + Self { + tool_rail_width: if width < 1050.0 { 94.0 } else { 136.0 }, + inspector_width: (width * 0.22).clamp(225.0, 300.0), + inspector_max: (width * 0.28).clamp(260.0, 350.0), + narrow: width < 1050.0, + } + } +} + +fn symbol(tool: Tool) -> &'static str { + match tool { + Tool::Line => "╱", + Tool::Arrow => "→", + Tool::Rectangle => "▭", + Tool::Circle => "○", + Tool::Node => "T", + Tool::Cubic => "∿", + } +} + +/// Actual tool selection uses the same Session and C catalogue as the +/// historical toolbar. It does not insert any TeX or create an Undo entry. +pub fn show_tool_rail( + ui: &mut egui::Ui, + session: &mut Session, + source: &str, + narrow: bool, +) -> Option { + let mut navigate = None; + ui.heading(if narrow { "TikZ" } else { "Инструменты" }); + ui.separator(); + for tool in TOOLS { + let info = tikz_authoring::spec(tool); + let title = if narrow { + symbol(tool).to_owned() + } else { + format!("{} {}", symbol(tool), info.title_ru) + }; + let response = ui + .add_sized( + [ui.available_width(), 34.0], + egui::Button::new(title).selected(session.active == Some(tool)), + ) + .on_hover_text(format!( + "{}\n{}\nPGF/TikZ §{} · {}\nБиблиотеки: {}", + info.help_ru, + info.syntax, + info.manual_section, + info.manual_url, + if info.requirements.libraries.is_empty() { + "core".to_owned() + } else { + info.requirements.libraries.join(", ") + } + )); + if response.clicked() { + session.select_tool(tool); + } + } + ui.separator(); + let tool_state = session.active.map(|tool| tikz_authoring::requirements_status(source, tool)); + match tool_state { + Some(Evidence::Confirmed) => { + ui.small("Код TikZ готов к вставке."); + } + Some(Evidence::Absent) => { + ui.colored_label(ui.visuals().warn_fg_color, "Нужно подключение."); + } + Some(Evidence::Unknown) => { + ui.colored_label(ui.visuals().warn_fg_color, "Импорт: UNKNOWN"); + } + None => { + ui.small("Выберите инструмент."); + } + } + ui.add_space(6.0); + if ui + .add_sized( + [ui.available_width(), 29.0], + egui::Button::new(if narrow { "Пакеты" } else { "Библиотеки / пакеты" }), + ) + .on_hover_text("Родные TikZ-библиотеки и другие пакеты; неизвестные возможности не скрываются.") + .clicked() + { + navigate = Some(StudioDock::Libraries); + } + if ui + .add_sized( + [ui.available_width(), 29.0], + egui::Button::new(if narrow { "F1" } else { "Справка · F1" }), + ) + .clicked() + { + session.help = true; + navigate = Some(StudioDock::Help); + } + ui.separator(); + ui.small("Редактируется обычный .tex; сетка не заменяет PDF."); + navigate +} + +/// One read-only context help dock shared with F1, command properties and +/// the offline search. No duplicate defaults or new parameter schema. +pub fn show_context_help(ui: &mut egui::Ui, session: &Session) { + ui.heading("Контекстная справка"); + if let Some(tool) = session.active { + let info = tikz_authoring::spec(tool); + ui.strong(&info.title_ru); + ui.label(&info.help_ru); + ui.code(&info.syntax); + ui.label("Пример из общего каталога:"); + ui.code(&info.example); + ui.small(format!( + "Требования: {} · библиотеки: {} · XeLaTeX · сверено с PGF {}", + info.requirements.packages.join(", "), + if info.requirements.libraries.is_empty() { + "core".to_owned() + } else { + info.requirements.libraries.join(", ") + }, + info.requirements.documented_against + )); + ui.hyperlink_to( + format!("PGF/TikZ §{}", info.manual_section), + &info.manual_url, + ); + for parameter in &info.parameter { + egui::CollapsingHeader::new(¶meter.id).show(ui, |ui| { + ui.label(¶meter.help_ru); + ui.code(¶meter.syntax); + ui.small(format!( + "Тип: {} · Единицы: {} · Значения: {}", + parameter.kind, + parameter.units.join(", "), + parameter.allowed_values.join(", ") + )); + if parameter.default_provenance == "editor_preset" { + ui.small(format!( + "Начальное значение редактора: {} (НЕ default TikZ)", + parameter.default + )); + } else { + ui.small("Документированный default: UNKNOWN"); + } + ui.label(¶meter.interaction_ru); + ui.label(¶meter.warning_ru); + ui.code(¶meter.example); + ui.hyperlink_to( + format!("PGF/TikZ §{}", parameter.manual_section), + ¶meter.manual_url, + ); + }); + } + } else { + ui.label("Выберите инструмент слева или найдите команду ниже."); + ui.small("Требования, значения и примеры берутся из единого каталога C."); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ergonomic_width_policy_keeps_a_real_central_viewport() { + for width in [850.0_f32, 1180.0, 1366.0, 1920.0] { + let l = StudioLayout::for_width(width); + // 240 pt editable source, chosen dock and its rail must not + // consume the entire minimum-sized studio. + assert!(width - l.tool_rail_width - l.inspector_width - 240.0 > 190.0); + } + assert!(StudioLayout::for_width(850.0).narrow); + assert!(!StudioLayout::for_width(1366.0).narrow); + } + + #[test] + fn rail_reuses_the_single_validated_c_catalogue() { + let ids: Vec<_> = TOOLS + .iter() + .map(|tool| tikz_authoring::spec(*tool).id.as_str()) + .collect(); + assert_eq!(ids.len(), 6); + assert!(ids.contains(&"tikz.bezier-cubic")); + assert!(ids.contains(&"tikz.arrow")); + } + + #[test] + fn studio_dock_labels_and_serialization_are_stable() { + assert_eq!(StudioDock::ALL.len(), 3); + let value = serde_json::to_string(&StudioDock::Libraries).unwrap(); + assert_eq!(serde_json::from_str::(&value).unwrap(), StudioDock::Libraries); + } +} diff --git a/crates/tikzarium-app/src/ui_theme.rs b/crates/tikzarium-app/src/ui_theme.rs index b74641de..038ea4cf 100644 --- a/crates/tikzarium-app/src/ui_theme.rs +++ b/crates/tikzarium-app/src/ui_theme.rs @@ -2,13 +2,14 @@ use eframe::egui::{self, Color32, Stroke}; use serde::{Deserialize, Serialize}; use tikzarium_core::FigureFonts; +use crate::studio_ui::StudioDock; pub const STORAGE_KEY: &str = "tikzarium.preferences.v1"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UiTheme { - Light, #[default] + Light, Dark, Midnight, Paper, @@ -71,6 +72,11 @@ impl UiTheme { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct UiPreferences { + pub studio_mode: bool, + pub studio_tool_rail: bool, + pub studio_inspector_open: bool, + pub studio_console_open: bool, + pub studio_dock: StudioDock, pub theme: UiTheme, pub compact: bool, pub show_font_lab: bool, @@ -83,8 +89,13 @@ impl Default for UiPreferences { fn default() -> Self { let fonts = FigureFonts::default(); Self { + studio_mode: true, + studio_tool_rail: true, + studio_inspector_open: true, + studio_console_open: false, + studio_dock: StudioDock::default(), theme: UiTheme::default(), - compact: true, + compact: false, show_font_lab: false, text_family: fonts.text_family, sans_family: fonts.sans_family, @@ -136,12 +147,15 @@ mod tests { #[test] fn preferences_default_and_legacy_partial_data() { let defaults = UiPreferences::default(); - assert!(defaults.compact); + assert!(!defaults.compact); + assert!(defaults.studio_mode); + assert_eq!(defaults.studio_dock, StudioDock::Properties); assert_eq!(defaults.math_font, "latinmodern-math.otf"); let old = r#"{"theme":"Paper"}"#; let restored: UiPreferences = serde_json::from_str(old).unwrap(); assert_eq!(restored.theme, UiTheme::Paper); - assert!(restored.compact); + assert!(!restored.compact); + assert!(restored.studio_mode); } #[test] From d0e75147b2d82598af3a199e3760ca20ba09e0c4 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 10:59:57 +0300 Subject: [PATCH 02/52] feat(B42): preserve TikZ pointer handlers while moving tool palette out of canvas --- crates/tikzarium-app/src/tikz_canvas.rs | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 9129e2d6..30e5736d 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -227,12 +227,27 @@ fn hit_authored_anchor(source: &str, rect: Rect, pos: Pos2) -> Result, +) -> (Option, Rect) { + show_guide_frame_mode(ui, session, source, caps, palette_rects, true) +} + +/// Both workspaces use EXACTLY the same gesture processing and source patching. +/// Studio moves the existing C-owned palette into a persistent tool rail; it +/// does not replace the native TikZ drawing handler or TeX with a Rust solver. +fn show_guide_frame_mode( ui: &mut egui::Ui, session: &mut Session, source: &mut String, caps: Capabilities, mut palette_rects: Option<&mut [Rect; 6]>, + legacy_palette: bool, ) -> (Option, Rect) { + if legacy_palette { ui.horizontal_wrapped(|ui| { ui.strong("TikZ · инструменты"); for (index, tool) in TOOLS.into_iter().enumerate() { @@ -256,6 +271,16 @@ fn show_guide_frame_with_palette( None => "ИНСТРУМЕНТ НЕ ВЫБРАН".to_owned(), }); ui.small("ПОЛЕ ВВОДА · шаг 0.25 cm. Esc отменяет клик/жест до отпускания мыши. Alt+клик на #ID выбирает команду. Это не PDF; стили и преобразования выполняет TeX."); + } else { + ui.horizontal(|ui| { + ui.strong("Эскиз TikZ"); + if let Some(tool) = session.active { + ui.small(format!("Выбран: {} · шаг 0,25 cm", tikz_authoring::spec(tool).title_ru)); + } else { + ui.small("Выберите инструмент в палитре слева."); + } + }); + } let project_state = session .active .map(|tool| tikz_authoring::requirements_status(source, tool)); @@ -562,6 +587,17 @@ pub fn show_guide( show_guide_frame(ui, session, source, caps).0 } +/// B42 studio guide: real egui pointer gestures and revision-checked C source +/// patches are preserved; only the duplicate row of tool buttons is removed. +pub fn show_guide_studio( + ui: &mut egui::Ui, + session: &mut Session, + source: &mut String, + caps: Capabilities, +) -> Option { + show_guide_frame_mode(ui, session, source, caps, None, false).0 +} + pub fn handle_help_shortcut(ctx: &egui::Context, session: &mut Session) { if ctx.input(|input| input.key_pressed(egui::Key::F1)) { session.help = !session.help; From a7e000b007d8c913efcf7e9d7b8742fb9147d7ca Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:05:19 +0300 Subject: [PATCH 03/52] feat(B42): native vector icon rail and grouped floating TikZ tool palettes --- crates/tikzarium-app/src/studio_ui.rs | 484 +++++++++++++++++--------- 1 file changed, 326 insertions(+), 158 deletions(-) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 898ac29d..0e084cfa 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -1,11 +1,10 @@ -//! B42 studio workbench: native TikZ tool rail and context-sensitive docks. -//! The C catalogue remains the single source of tool/help facts, and the -//! external XeLaTeX PDF remains authoritative. No internal TeX renderer. -use eframe::egui; +//! B42: grouped icon palettes for TikZ-native authoring. +//! Tool and parameter semantics belong to the existing C catalogue. Vector +//! icon strokes are only UI affordances, never a second PGF renderer. +use eframe::egui::{self, Color32, Pos2, Rect, Sense, Shape, Stroke, Vec2}; use serde::{Deserialize, Serialize}; use tikzarium_extensions::primitives::Evidence; - -use crate::tikz_authoring::{self, Session, TOOLS, Tool}; +use crate::tikz_authoring::{self, Session, Tool}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum StudioDock { @@ -14,10 +13,8 @@ pub enum StudioDock { Libraries, Help, } - impl StudioDock { pub const ALL: [Self; 3] = [Self::Properties, Self::Libraries, Self::Help]; - pub const fn label(self) -> &'static str { match self { Self::Properties => "Свойства", @@ -27,8 +24,37 @@ impl StudioDock { } } -/// Layout policy uses logical egui points, not PDF coordinates. At narrow -/// widths the rail becomes icon-only, and the dock can be hidden explicitly. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StudioGroup { + Geometry, + Connections, + Text, + Extensions, +} +impl StudioGroup { + pub const ALL: [Self; 4] = [ + Self::Geometry, + Self::Connections, + Self::Text, + Self::Extensions, + ]; + pub const fn label(self) -> &'static str { + match self { + Self::Geometry => "Геометрия", + Self::Connections => "Линии и стрелки", + Self::Text => "Узлы и подписи", + Self::Extensions => "Расширения TikZ", + } + } + pub const fn tools(self) -> &'static [Tool] { + match self { + Self::Geometry => &[Tool::Line, Tool::Rectangle, Tool::Circle, Tool::Cubic], + Self::Connections => &[Tool::Arrow], + Self::Text => &[Tool::Node], + Self::Extensions => &[], + } + } +} #[derive(Clone, Copy, Debug)] pub struct StudioLayout { pub tool_rail_width: f32, @@ -36,13 +62,11 @@ pub struct StudioLayout { pub inspector_max: f32, pub narrow: bool, } - impl StudioLayout { - #[must_use] pub fn for_width(width: f32) -> Self { let width = if width.is_finite() { width.max(0.0) } else { 0.0 }; Self { - tool_rail_width: if width < 1050.0 { 94.0 } else { 136.0 }, + tool_rail_width: 62.0, inspector_width: (width * 0.22).clamp(225.0, 300.0), inspector_max: (width * 0.28).clamp(260.0, 350.0), narrow: width < 1050.0, @@ -50,187 +74,331 @@ impl StudioLayout { } } -fn symbol(tool: Tool) -> &'static str { - match tool { - Tool::Line => "╱", - Tool::Arrow => "→", - Tool::Rectangle => "▭", - Tool::Circle => "○", - Tool::Node => "T", - Tool::Cubic => "∿", +#[derive(Clone, Copy)] +pub enum StudioIcon { + Tool(Tool), + Group(StudioGroup), + Open, + Save, + Build, + Export, + Help, + Console, +} +fn line(p: &egui::Painter, a: Pos2, b: Pos2, stroke: Stroke) { + p.line_segment([a, b], stroke); +} +fn draw_icon(ui: &egui::Ui, rect: Rect, kind: StudioIcon, selected: bool) { + let p = ui.painter_at(rect); + let c = rect.center(); + let t = rect.width().min(rect.height()) * 0.28; + let stroke = Stroke::new(1.9, if selected { + ui.visuals().selection.stroke.color + } else { + ui.visuals().text_color() + }); + let point = |x: f32, y: f32| Pos2::new(c.x + t * x, c.y + t * y); + match kind { + StudioIcon::Tool(Tool::Line) => { + line(&p, point(-1.0, 0.9), point(1.0, -0.9), stroke); + p.circle_filled(point(-1.0, 0.9), 2.3, stroke.color); + p.circle_filled(point(1.0, -0.9), 2.3, stroke.color); + } + StudioIcon::Tool(Tool::Arrow) => { + line(&p, point(-1.0, 0.0), point(1.0, 0.0), stroke); + line(&p, point(1.0, 0.0), point(0.3, -0.65), stroke); + line(&p, point(1.0, 0.0), point(0.3, 0.65), stroke); + } + StudioIcon::Tool(Tool::Rectangle) => { + let a=point(-1.0,-0.72); let b=point(1.0,0.72); + p.add(Shape::closed_line(vec![ + a, point(1.0,-0.72), b, point(-1.0,0.72) + ],stroke)); + } + StudioIcon::Tool(Tool::Circle) => { + p.circle_stroke(c, t*0.9, stroke); + } + StudioIcon::Tool(Tool::Node) => { + p.add(Shape::closed_line(vec![ + point(-1.0,-0.8),point(1.0,-0.8), + point(1.0,0.8),point(-1.0,0.8) + ], stroke)); + p.text(c,egui::Align2::CENTER_CENTER,"T", + egui::FontId::proportional(t*1.15),stroke.color); + } + StudioIcon::Tool(Tool::Cubic) => { + let mut points=Vec::with_capacity(21); + for i in 0..=20 { + let v=i as f32/20.0;let inv=1.0-v; + let x=inv*inv*inv*(-1.1)+3.0*inv*inv*v*(-0.45) + +3.0*inv*v*v*(0.45)+v*v*v*(1.1); + let y=inv*inv*inv*(0.8)+3.0*inv*inv*v*(-1.3) + +3.0*inv*v*v*(1.3)+v*v*v*(-0.8); + points.push(point(x,y)); + } + p.add(Shape::line(points,stroke)); + } + StudioIcon::Group(StudioGroup::Geometry) => { + p.circle_stroke(point(-0.45,-0.4),t*0.60,stroke); + p.add(Shape::closed_line(vec![ + point(0.10,-0.10),point(1.0,-0.10), + point(1.0,0.82),point(0.10,0.82) + ],stroke)); + } + StudioIcon::Group(StudioGroup::Connections) => + draw_icon(ui,rect,StudioIcon::Tool(Tool::Arrow),selected), + StudioIcon::Group(StudioGroup::Text) => + draw_icon(ui,rect,StudioIcon::Tool(Tool::Node),selected), + StudioIcon::Group(StudioGroup::Extensions) => { + for (x,y) in [(-0.65,-0.65),(0.65,-0.65),(-0.65,0.65),(0.65,0.65)] { + p.circle_filled(point(x,y),2.6,stroke.color); + } + line(&p,point(-0.65,-0.65),point(0.65,0.65),stroke); + line(&p,point(-0.65,0.65),point(0.65,-0.65),stroke); + } + StudioIcon::Open => { + p.add(Shape::line(vec![ + point(-1.05,-0.45),point(-0.45,-0.45), + point(-0.18,-0.15),point(0.95,-0.15), + point(0.85,0.76),point(-0.98,0.76), + point(-1.05,-0.45) + ],stroke)); + } + StudioIcon::Save => { + p.add(Shape::closed_line(vec![ + point(-0.85,-1.0),point(0.58,-1.0), + point(0.95,-0.65),point(0.95,1.0), + point(-0.85,1.0) + ],stroke)); + line(&p,point(-0.43,-0.9),point(-0.43,-0.3),stroke); + line(&p,point(-0.43,-0.3),point(0.38,-0.3),stroke); + line(&p,point(-0.43,0.25),point(0.55,0.25),stroke); + } + StudioIcon::Build => { + p.add(Shape::closed_line(vec![ + point(-0.65,-1.0),point(1.0,0.0),point(-0.65,1.0) + ],stroke)); + } + StudioIcon::Export => { + p.add(Shape::line(vec![ + point(-0.9,-0.3),point(-0.9,0.98), + point(0.9,0.98),point(0.9,-0.3) + ],stroke)); + line(&p,point(0.0,-1.0),point(0.0,0.38),stroke); + line(&p,point(0.0,0.38),point(-0.5,-0.13),stroke); + line(&p,point(0.0,0.38),point(0.5,-0.13),stroke); + } + StudioIcon::Help => { + p.circle_stroke(c,t,stroke); + p.text(c,egui::Align2::CENTER_CENTER,"?", + egui::FontId::proportional(t*1.6),stroke.color); + } + StudioIcon::Console => { + p.add(Shape::closed_line(vec![ + point(-1.0,-0.8),point(1.0,-0.8), + point(1.0,0.8),point(-1.0,0.8) + ],stroke)); + line(&p,point(-0.65,-0.3),point(-0.2,0.0),stroke); + line(&p,point(-0.2,0.0),point(-0.65,0.3),stroke); + line(&p,point(-0.05,0.35),point(0.65,0.35),stroke); + } } } -/// Actual tool selection uses the same Session and C catalogue as the -/// historical toolbar. It does not insert any TeX or create an Undo entry. -pub fn show_tool_rail( +/// Scalable vector icon. A short, stable tooltip includes the native TikZ +/// command and its actual C-owned manual reference. +pub fn icon_button( ui: &mut egui::Ui, - session: &mut Session, - source: &str, - narrow: bool, -) -> Option { - let mut navigate = None; - ui.heading(if narrow { "TikZ" } else { "Инструменты" }); - ui.separator(); - for tool in TOOLS { - let info = tikz_authoring::spec(tool); - let title = if narrow { - symbol(tool).to_owned() + kind: StudioIcon, + selected: bool, + side: f32, + tooltip: &str, +) -> egui::Response { + let (rect,response)=ui.allocate_exact_size(Vec2::splat(side),Sense::click()); + if ui.is_rect_visible(rect) { + let p=ui.painter_at(rect); + let fill=if selected { + ui.visuals().selection.bg_fill + } else if response.hovered() { + ui.visuals().widgets.hovered.bg_fill } else { - format!("{} {}", symbol(tool), info.title_ru) + ui.visuals().widgets.inactive.bg_fill }; - let response = ui - .add_sized( - [ui.available_width(), 34.0], - egui::Button::new(title).selected(session.active == Some(tool)), - ) - .on_hover_text(format!( - "{}\n{}\nPGF/TikZ §{} · {}\nБиблиотеки: {}", - info.help_ru, - info.syntax, - info.manual_section, - info.manual_url, - if info.requirements.libraries.is_empty() { - "core".to_owned() - } else { - info.requirements.libraries.join(", ") - } - )); - if response.clicked() { - session.select_tool(tool); - } + p.rect_filled(rect,egui::CornerRadius::same(7),fill); + draw_icon(ui,rect,kind,selected); } - ui.separator(); - let tool_state = session.active.map(|tool| tikz_authoring::requirements_status(source, tool)); - match tool_state { - Some(Evidence::Confirmed) => { - ui.small("Код TikZ готов к вставке."); - } - Some(Evidence::Absent) => { - ui.colored_label(ui.visuals().warn_fg_color, "Нужно подключение."); - } - Some(Evidence::Unknown) => { - ui.colored_label(ui.visuals().warn_fg_color, "Импорт: UNKNOWN"); - } - None => { - ui.small("Выберите инструмент."); + response.on_hover_text(tooltip) +} + +/// Narrow permanent rail: four *groups* rather than dozens of buttons. +/// Clicking a group opens a floating palette; the code and PDF keep their size. +pub fn show_group_rail( + ui: &mut egui::Ui, + active: &mut Option, + narrow: bool, +) -> Option { + let _=narrow; + ui.vertical_centered(|ui|{ + ui.strong("TikZ"); + ui.separator(); + }); + let mut anchor=None; + for group in StudioGroup::ALL { + let tooltip=format!("Палитра: {}. Нажмите для раскрытия.",group.label()); + let response=icon_button(ui,StudioIcon::Group(group),*active==Some(group),43.0,&tooltip); + if response.clicked() { + *active=if *active==Some(group){None}else{Some(group)}; + anchor=Some(response.rect.right_top()); } } - ui.add_space(6.0); - if ui - .add_sized( - [ui.available_width(), 29.0], - egui::Button::new(if narrow { "Пакеты" } else { "Библиотеки / пакеты" }), - ) - .on_hover_text("Родные TikZ-библиотеки и другие пакеты; неизвестные возможности не скрываются.") - .clicked() - { - navigate = Some(StudioDock::Libraries); - } - if ui - .add_sized( - [ui.available_width(), 29.0], - egui::Button::new(if narrow { "F1" } else { "Справка · F1" }), - ) - .clicked() - { - session.help = true; - navigate = Some(StudioDock::Help); + anchor +} + +/// Floating grouped palette. It never takes space from the code/PDF columns. +/// Future package adapters appear as unavailable, not hidden or falsely ready. +pub fn show_palette( + ctx: &egui::Context, + group: &mut Option, + anchor: Pos2, + session: &mut Session, + source: &str, + open_libraries: &mut bool, +) { + if ctx.input(|input| input.key_pressed(egui::Key::Escape)){ + *group=None; } - ui.separator(); - ui.small("Редактируется обычный .tex; сетка не заменяет PDF."); - navigate + let Some(selected)=*group else {return}; + egui::Area::new(egui::Id::new("b42-group-palette")) + .order(egui::Order::Foreground) + .fixed_pos(anchor+egui::vec2(6.0,-36.0)) + .show(ctx, |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + ui.set_min_width(158.0); + ui.horizontal(|ui|{ + ui.strong(selected.label()); + if ui.small_button("×").on_hover_text("Закрыть палитру").clicked(){ + *group=None; + } + }); + ui.separator(); + if selected==StudioGroup::Extensions { + ui.small("Пакеты / библиотеки"); + for name in ["PGFPlots","CircuitikZ","tikz-cd"] { + ui.add_enabled(false,egui::Button::new(name)) + .on_disabled_hover_text("Графический адаптер ещё не реализован. Обычный исходный TeX остаётся доступен."); + } + if ui.button("Показать все пакеты").clicked(){ + *open_libraries=true; + *group=None; + } + } else { + ui.horizontal_wrapped(|ui|{ + for &tool in selected.tools() { + let info=tikz_authoring::spec(tool); + let status=tikz_authoring::requirements_status(source,tool); + let tip=format!( + "{}\n{}\n{}\nPGF/TikZ §{}\nПодключение: {:?}", + info.title_ru,info.help_ru,info.syntax,info.manual_section,status + ); + if icon_button( + ui,StudioIcon::Tool(tool),session.active==Some(tool),46.0,&tip + ).clicked() { + session.select_tool(tool); + *group=None; + } + } + }); + ui.small("Наведите на иконку для команды и справки."); + if selected==StudioGroup::Connections { + ui.small("Стрелки arrows.meta требуют явного подключения."); + } + } + }); + }); } -/// One read-only context help dock shared with F1, command properties and -/// the offline search. No duplicate defaults or new parameter schema. -pub fn show_context_help(ui: &mut egui::Ui, session: &Session) { - ui.heading("Контекстная справка"); - if let Some(tool) = session.active { - let info = tikz_authoring::spec(tool); +/// The same read-only C catalogue consumed by the inspector and F1. +pub fn show_context_help(ui:&mut egui::Ui,session:&Session) { + ui.heading("Справка TikZ"); + if let Some(tool)=session.active { + let info=tikz_authoring::spec(tool); ui.strong(&info.title_ru); ui.label(&info.help_ru); ui.code(&info.syntax); - ui.label("Пример из общего каталога:"); + ui.label("Пример:"); ui.code(&info.example); + ui.hyperlink_to(format!("PGF/TikZ §{}",info.manual_section),&info.manual_url); ui.small(format!( - "Требования: {} · библиотеки: {} · XeLaTeX · сверено с PGF {}", + "Пакет: {} · Библиотеки: {} · PGF {}", info.requirements.packages.join(", "), - if info.requirements.libraries.is_empty() { - "core".to_owned() - } else { - info.requirements.libraries.join(", ") - }, + info.requirements.libraries.join(", "), info.requirements.documented_against )); - ui.hyperlink_to( - format!("PGF/TikZ §{}", info.manual_section), - &info.manual_url, - ); - for parameter in &info.parameter { - egui::CollapsingHeader::new(¶meter.id).show(ui, |ui| { - ui.label(¶meter.help_ru); - ui.code(¶meter.syntax); + for param in &info.parameter{ + egui::CollapsingHeader::new(¶m.id).show(ui,|ui|{ + ui.label(¶m.help_ru); + ui.code(¶m.syntax); ui.small(format!( - "Тип: {} · Единицы: {} · Значения: {}", - parameter.kind, - parameter.units.join(", "), - parameter.allowed_values.join(", ") + "Тип: {} · Единицы: {} · Допустимые значения: {}", + param.kind,param.units.join(", "),param.allowed_values.join(", ") )); - if parameter.default_provenance == "editor_preset" { + if param.default_provenance=="editor_preset" { ui.small(format!( - "Начальное значение редактора: {} (НЕ default TikZ)", - parameter.default + "Предустановка редактора: {} (НЕ default TikZ)",param.default )); - } else { - ui.small("Документированный default: UNKNOWN"); - } - ui.label(¶meter.interaction_ru); - ui.label(¶meter.warning_ru); - ui.code(¶meter.example); - ui.hyperlink_to( - format!("PGF/TikZ §{}", parameter.manual_section), - ¶meter.manual_url, - ); + }else{ui.small("Default TikZ: UNKNOWN");} + ui.label(¶m.warning_ru); + ui.label(¶m.interaction_ru); + ui.code(¶m.example); + ui.hyperlink_to(format!("PGF/TikZ §{}",param.manual_section),¶m.manual_url); }); } - } else { - ui.label("Выберите инструмент слева или найдите команду ниже."); - ui.small("Требования, значения и примеры берутся из единого каталога C."); + }else{ + ui.label("Выберите инструмент в левой палитре."); + ui.small("Подсказки и параметры поступают из единого каталога C."); } } #[cfg(test)] mod tests { use super::*; - #[test] - fn ergonomic_width_policy_keeps_a_real_central_viewport() { - for width in [850.0_f32, 1180.0, 1366.0, 1920.0] { - let l = StudioLayout::for_width(width); - // 240 pt editable source, chosen dock and its rail must not - // consume the entire minimum-sized studio. - assert!(width - l.tool_rail_width - l.inspector_width - 240.0 > 190.0); - } - assert!(StudioLayout::for_width(850.0).narrow); - assert!(!StudioLayout::for_width(1366.0).narrow); + fn compact_group_rail_is_bounded_with_expansion_as_floating_overlay() { + for width in [850.0_f32,1180.0,1366.0,1920.0] { + let layout=StudioLayout::for_width(width); + assert_eq!(layout.tool_rail_width,62.0); + assert!(width-layout.tool_rail_width-layout.inspector_width-240.0>190.0); + } + } + #[test] + fn groups_cover_all_native_c_tools_once_and_expose_future_adapters() { + let all=StudioGroup::ALL.iter().flat_map(|g|g.tools()).copied() + .collect::>(); + assert_eq!(all.len(),6); + for item in crate::tikz_authoring::TOOLS{ + assert_eq!(all.iter().filter(|&&t|t==item).count(),1); + assert!(tikz_authoring::spec(item).manual_url.starts_with("https://tikz.dev/")); + } + assert!(StudioGroup::Extensions.tools().is_empty()); } - #[test] - fn rail_reuses_the_single_validated_c_catalogue() { - let ids: Vec<_> = TOOLS - .iter() - .map(|tool| tikz_authoring::spec(*tool).id.as_str()) - .collect(); - assert_eq!(ids.len(), 6); - assert!(ids.contains(&"tikz.bezier-cubic")); - assert!(ids.contains(&"tikz.arrow")); + fn icon_tooltips_and_dock_types_do_not_duplicate_document_state() { + let v=serde_json::to_string(&StudioDock::Libraries).unwrap(); + assert_eq!(serde_json::from_str::(&v).unwrap(),StudioDock::Libraries); + assert_eq!(StudioDock::ALL.len(),3); } - #[test] - fn studio_dock_labels_and_serialization_are_stable() { - assert_eq!(StudioDock::ALL.len(), 3); - let value = serde_json::to_string(&StudioDock::Libraries).unwrap(); - assert_eq!(serde_json::from_str::(&value).unwrap(), StudioDock::Libraries); + fn icon_geometry_does_not_change_source_in_egui_frame() { + let ctx=egui::Context::default(); + let mut source=String::from("% opaque CRLF\r\n\\newcommand{\\mine}{value}\r\n"); + let before=source.clone(); + let _=ctx.run(egui::RawInput::default(),|ctx|{ + egui::CentralPanel::default().show(ctx,|ui|{ + icon_button(ui,StudioIcon::Tool(Tool::Cubic),false,44.0,"Bezier"); + icon_button(ui,StudioIcon::Group(StudioGroup::Extensions),false,44.0,"Packages"); + }); + }); + assert_eq!(source,before); + source.clear(); } } From e514a5474bca0f5a6cf5f346b51a869bcb760131 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:06:53 +0300 Subject: [PATCH 04/52] feat(B42): connect icon-group palettes, contextual docks and clean studio toolbar to egui --- crates/tikzarium-app/src/main.rs | 273 ++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 9 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index c34f5f51..f1ed2704 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -15,6 +15,7 @@ mod font_proof; mod pdf_navigation; mod preview_image; mod source_fonts; +mod studio_ui; mod tikz_authoring; mod tikz_canvas; mod tikz_libraries; @@ -31,6 +32,7 @@ use pdf_navigation::{ }; use preview_image::{PreviewLodReceiver, decode_preview_png, spawn_preview_lod}; use source_fonts::scan_explicit_font_hints; +use studio_ui::{StudioDock, StudioGroup, StudioIcon}; use ui_theme::{STORAGE_KEY, UiPreferences}; const QUICKSTART: &str = include_str!("../../../examples/quickstart.tex"); @@ -466,6 +468,8 @@ struct TikzariumApp { pending_open_save_path: String, pending_open_validation_error: Option, prefs: UiPreferences, + studio_group: Option, + studio_palette_anchor: egui::Pos2, authoring: tikz_authoring::Session, highlight_generated_line: bool, // A source patch or document replacement must not leave native TextEdit @@ -526,6 +530,8 @@ impl TikzariumApp { pending_open_save_path: String::new(), pending_open_validation_error: None, prefs, + studio_group: None, + studio_palette_anchor: egui::pos2(65.0, 120.0), authoring: { let mut state = tikz_authoring::Session::default(); state.reset(QUICKSTART); @@ -1097,12 +1103,191 @@ impl TikzariumApp { ); } + /// The main toolbar is four clear vector icons, not a long wall of + /// textual action buttons. Infrequent commands live in two menus. + fn show_studio_toolbar(&mut self, ctx: &egui::Context) { + let ready = self.receiver.is_none() && self.pending_open.is_none(); + egui::TopBottomPanel::top("b42-studio-toolbar").show(ctx, |ui| { + if self.pending_open.is_some() { + ui.disable(); + } + ui.horizontal(|ui| { + ui.strong(egui::RichText::new("TikZarium").size(20.0)); + ui.separator(); + if studio_ui::icon_button( + ui, StudioIcon::Open, false, 34.0, "Открыть TeX · Ctrl+O" + ).clicked() && ready { + self.open_document_dialog(); + } + if studio_ui::icon_button( + ui, StudioIcon::Save, false, 34.0, "Сохранить исходник · Ctrl+S" + ).clicked() && ready { + self.save(); + } + if studio_ui::icon_button( + ui, StudioIcon::Build, self.receiver.is_some(), 34.0, + "Собрать точный PDF через внешний XeLaTeX · Ctrl+Enter" + ).clicked() && ready { + self.build(); + } + if studio_ui::icon_button( + ui, StudioIcon::Export, false, 34.0, + "Экспорт PDF. Только актуальный результат XeLaTeX." + ).clicked() && self.pdf.is_some() && !self.preview_stale() { + self.export(); + } + if self.receiver.is_some() && ui.small_button("■").on_hover_text("Отменить сборку").clicked() + && let Some(cancel) = &self.cancellation { + cancel.cancel(); + } + ui.separator(); + ui.menu_button("Файл", |ui| { + ui.label("TeX-проект:"); + ui.add( + egui::TextEdit::singleline(&mut self.file_field) + .desired_width(260.0) + .hint_text("figure.tex"), + ); + if ui.add_enabled(ready, egui::Button::new("Открыть путь")).clicked() { + self.open_path(PathBuf::from(self.file_field.trim())); + ui.close(); + } + if ui.add_enabled(ready, egui::Button::new("Сохранить как…")).clicked() { + self.save_as(); + ui.close(); + } + }); + ui.menu_button("Вид", |ui| { + ui.checkbox(&mut self.prefs.studio_tool_rail, "Группы инструментов"); + ui.checkbox(&mut self.prefs.studio_inspector_open, "Инспектор"); + ui.checkbox(&mut self.prefs.studio_console_open, "Диагностика"); + ui.separator(); + self.prefs.draw_toolbar(ui, ctx); + if ui.button("Классический режим").clicked() { + self.prefs.studio_mode = false; + ui.close(); + } + if ui.button("Шрифты рисунка").clicked() { + self.prefs.show_font_lab = true; + ui.close(); + } + }); + if studio_ui::icon_button( + ui, StudioIcon::Help, self.prefs.studio_dock == StudioDock::Help, 32.0, + "Контекстная справка F1 из общего TikZ-каталога" + ).clicked() { + self.authoring.help = true; + self.prefs.studio_dock = StudioDock::Help; + self.prefs.studio_inspector_open = true; + } + if studio_ui::icon_button( + ui, StudioIcon::Console, self.prefs.studio_console_open, 32.0, + "Показать или скрыть диагностику и журнал XeLaTeX" + ).clicked() { + self.prefs.studio_console_open = !self.prefs.studio_console_open; + } + }); + ui.horizontal(|ui| { + ui.selectable_value(&mut self.pdf_focus_in_split, false, "Эскиз"); + ui.selectable_value(&mut self.pdf_focus_in_split, true, "PDF"); + if self.pdf_focus_in_split { + self.fit_page_active = true; + } + ui.separator(); + let filename = self.document.path.as_ref() + .map_or_else(|| "Новый .tex".to_owned(), |p| p.display().to_string()); + ui.add(egui::Label::new(filename).truncate()); + if self.document.dirty() { + ui.colored_label(ui.visuals().warn_fg_color, "● Изменено"); + } + if self.preview_stale() { + ui.colored_label(ui.visuals().warn_fg_color, "PDF устарел"); + } + }); + }); + } + + /// One contextual dock at a time. Unlike the legacy UI, library inventory, + /// parameter editing and full help do not stack into one endless scroll. + fn show_studio_inspector(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + for tab in StudioDock::ALL { + ui.selectable_value(&mut self.prefs.studio_dock, tab, tab.label()); + } + }); + ui.separator(); + let prior = self.authoring.source_revision(); + if let Some(line) = tikz_canvas::show_inspector_action_bar( + ui, &mut self.authoring, &mut self.document.source + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; + } + fence_source_editor_undo_on_revision_change( + prior, &self.authoring, &mut self.reset_source_editor_undo_history + ); + ui.separator(); + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + match self.prefs.studio_dock { + StudioDock::Properties => { + let prior = self.authoring.source_revision(); + let caps = self.primitive_capabilities(); + if let Some(line) = tikz_canvas::show_inspector( + ui, &mut self.authoring, &mut self.document.source, caps + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; + } + fence_source_editor_undo_on_revision_change( + prior, &self.authoring, &mut self.reset_source_editor_undo_history + ); + } + StudioDock::Libraries => { + ui.heading("Библиотеки проекта"); + ui.small("Подключение — показанная обратимая правка преамбулы; не установка."); + let prior = self.authoring.source_revision(); + if let Some(line) = tikz_libraries::show_library_panel( + ui, &mut self.authoring, &mut self.document.source, + self.tex.xelatex.is_some() + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; + } + fence_source_editor_undo_on_revision_change( + prior, &self.authoring, &mut self.reset_source_editor_undo_history + ); + ui.collapsing("Реестр доступности пакетов", |ui| { + for item in &self.package_statuses { + ui.strong(&item.manifest.display_name); + ui.small(format!( + "Установка: {:?} · XeLaTeX: {:?} · GUI-адаптер: {}", + item.installation, item.compilation, + if item.visual_editable { "да" } else { "нет" } + )); + } + }); + } + StudioDock::Help => { + tikz_libraries::show_help_search(ui, &mut self.help_query); + ui.separator(); + studio_ui::show_context_help(ui, &self.authoring); + } + } + }); + } + fn show_pdf_canvas(&mut self, ui: &mut egui::Ui) { let canvas_width = ui.available_width(); let mut displayed_zoom_percent = pdf_zoom_percent(self.zoom); ui.horizontal_wrapped(|ui| { - ui.heading("Canvas · exact PDF (read-only)"); - if self.workspace == WorkspaceMode::Split { + ui.heading(if self.prefs.studio_mode { + "Рисунок / PDF" + } else { + "Canvas · exact PDF (read-only)" + }); + if self.workspace == WorkspaceMode::Split && !self.prefs.studio_mode { ui.label("Центр:"); show_split_center_focus_switch( ui, @@ -1178,13 +1363,22 @@ impl TikzariumApp { } ui.label(&self.preview_status); ui.small(&self.page_navigation_status); - ui.small("The guide pad edits N1-owned source through the shared TikZ catalogue. This exact PDF is read-only and is NOT coordinate-aligned with the guide."); + if !self.prefs.studio_mode { + ui.small("The guide pad edits N1-owned source through the shared TikZ catalogue. This exact PDF is read-only and is NOT coordinate-aligned with the guide."); + } if self.workspace == WorkspaceMode::Split && !self.pdf_focus_in_split { let caps = self.primitive_capabilities(); let previous_revision = self.authoring.source_revision(); - if let Some(line) = - tikz_canvas::show_guide(ui, &mut self.authoring, &mut self.document.source, caps) - { + let inserted = if self.prefs.studio_mode { + tikz_canvas::show_guide_studio( + ui, &mut self.authoring, &mut self.document.source, caps + ) + } else { + tikz_canvas::show_guide( + ui, &mut self.authoring, &mut self.document.source, caps + ) + }; + if let Some(line) = inserted { self.jump_to_line = Some(line); self.highlight_generated_line = true; // The source SidePanel has already been rendered this frame. @@ -1427,7 +1621,12 @@ impl eframe::App for TikzariumApp { // Pending dirty-Open decisions own the keyboard: do not change source // through a global help/visual Undo shortcut behind the modal. if self.pending_open.is_none() { + let had_help = self.authoring.help; tikz_canvas::handle_help_shortcut(ctx, &mut self.authoring); + if self.prefs.studio_mode && !had_help && self.authoring.help { + self.prefs.studio_dock = StudioDock::Help; + self.prefs.studio_inspector_open = true; + } if self.workspace == WorkspaceMode::Split && let Some(line) = tikz_canvas::handle_visual_undo_shortcut( ctx, @@ -1466,6 +1665,10 @@ impl eframe::App for TikzariumApp { } } let file_actions_enabled = self.receiver.is_none() && self.pending_open.is_none(); + if self.prefs.studio_mode { + self.workspace = WorkspaceMode::Split; + self.show_studio_toolbar(ctx); + } else { egui::TopBottomPanel::top("tools").show(ctx, |ui| { if self.pending_open.is_some() { ui.disable(); @@ -1556,8 +1759,21 @@ impl eframe::App for TikzariumApp { ui.label(&self.status); }); }); + } let split_layout = SplitPanelLayout::for_width(ctx.available_rect().width()); - if self.workspace == WorkspaceMode::Split { + if self.prefs.studio_mode && self.prefs.studio_inspector_open { + let studio = studio_ui::StudioLayout::for_width(ctx.available_rect().width()); + egui::SidePanel::right("b42-studio-inspector") + .resizable(true) + .default_width(studio.inspector_width) + .min_width(225.0) + .max_width(studio.inspector_max) + .show(ctx, |ui| { + if self.pending_open.is_some() { ui.disable(); } + self.show_studio_inspector(ui); + }); + } + if !self.prefs.studio_mode && self.workspace == WorkspaceMode::Split { show_split_inspector_panel(ctx, split_layout, |ui| { let previous_revision = self.authoring.source_revision(); if self.pending_open.is_some() { @@ -1582,7 +1798,8 @@ impl eframe::App for TikzariumApp { .show(ui, |ui| self.show_authoring_inspector(ui)); }); } - if self.show_log { + if (!self.prefs.studio_mode && self.show_log) + || (self.prefs.studio_mode && self.prefs.studio_console_open) { egui::TopBottomPanel::bottom("log") .resizable(true) .default_height(145.0) @@ -1657,6 +1874,21 @@ impl eframe::App for TikzariumApp { }); }); } + if self.prefs.studio_mode && self.workspace == WorkspaceMode::Split + && self.prefs.studio_tool_rail { + egui::SidePanel::left("b42-tool-groups") + .resizable(false) + .exact_width(studio_ui::StudioLayout::for_width(ctx.available_rect().width()).tool_rail_width) + .show(ctx, |ui| { + if self.pending_open.is_some() { ui.disable(); } + if let Some(anchor) = studio_ui::show_group_rail( + ui, &mut self.studio_group, + studio_ui::StudioLayout::for_width(ctx.available_rect().width()).narrow, + ) { + self.studio_palette_anchor = anchor; + } + }); + } if self.workspace == WorkspaceMode::Split { show_split_source_panel(ctx, split_layout, |ui| { self.show_source_editor(ui); @@ -1672,6 +1904,29 @@ impl eframe::App for TikzariumApp { self.show_source_editor(ui); } }); + if self.prefs.studio_mode && self.pending_open.is_none() { + let mut open_libraries = false; + studio_ui::show_palette( + ctx, &mut self.studio_group, self.studio_palette_anchor, + &mut self.authoring, &self.document.source, &mut open_libraries, + ); + if open_libraries { + self.prefs.studio_dock = StudioDock::Libraries; + self.prefs.studio_inspector_open = true; + } + } + if self.prefs.studio_mode { + egui::TopBottomPanel::bottom("b42-status").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new(&self.status).small()); + if self.tex.xelatex.is_none() { + ui.colored_label(ui.visuals().warn_fg_color, "XeLaTeX не выбран"); + } else { + ui.small("XeLaTeX: настроен"); + } + }); + }); + } if let Some(path) = self.pending_open.clone() { let previous_destination = self.pending_open_save_path.clone(); let decision = show_pending_open_dialog( @@ -1724,7 +1979,7 @@ impl eframe::App for TikzariumApp { fn main() -> eframe::Result<()> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size(egui::vec2(1250.0, 800.0)) + .with_inner_size(egui::vec2(1366.0, 768.0)) .with_min_inner_size(egui::vec2(850.0, 560.0)), ..Default::default() }; From 1244b642609b31cdbb7dc73aa76983ad51219aed Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:11:29 +0300 Subject: [PATCH 05/52] test(B42): user Windows Server external MiKTeX offline PDF and locked Rust QA --- scripts/tools/qa_windows_server_user.ps1 | 116 +++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 scripts/tools/qa_windows_server_user.ps1 diff --git a/scripts/tools/qa_windows_server_user.ps1 b/scripts/tools/qa_windows_server_user.ps1 new file mode 100644 index 00000000..1c0428fb --- /dev/null +++ b/scripts/tools/qa_windows_server_user.ps1 @@ -0,0 +1,116 @@ +# User-installed external MiKTeX on persistent Windows Server. +# NO installer, package writes, registry settings or hidden engine fallback. +[CmdletBinding()] +param([Parameter(Mandatory)][string]$OutputDir,[switch]$CheckRust) +$ErrorActionPreference='Stop' +New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null +$reportPath=Join-Path $OutputDir 'windows-server-qa.json' +$report=[ordered]@{ + schema_version=1;status='INCOMPLETE';environment='USER_WINDOWS_SERVER' + physical_windows10='NOT_RUN';mac_m1_m4='NOT_RUN';interactive_gui='NOT_RUN' + cargo='NOT_RUN';font_audit='NOT_RUN';package_autoinstall='DISABLED_BY_FLAG' +} +$exit=1 +try { + $root=(Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path + Push-Location $root + try { + $report.sha=(& git rev-parse HEAD | Out-String).Trim() + if($LASTEXITCODE -ne 0 -or $report.sha -notmatch '^[0-9a-f]{40}$'){throw 'Git SHA not established'} + $os=Get-CimInstance Win32_OperatingSystem + $report.os=$os.Caption;$report.build=$os.BuildNumber + $engine=$env:TIKZARIUM_XELATEX + if([string]::IsNullOrWhiteSpace($engine)){ + $cmd=Get-Command xelatex -ErrorAction SilentlyContinue + if(-not $cmd){throw 'External XeLaTeX missing; no fallback'} + $engine=$cmd.Source + } + if(-not (Test-Path -LiteralPath $engine -PathType Leaf)){throw 'Selected XeLaTeX does not exist'} + $banner=(& $engine '--disable-installer' '--version' 2>&1 | Out-String) + if($LASTEXITCODE -ne 0 -or $banner -notmatch 'MiKTeX-XeTeX'){ + throw 'Only verified MiKTeX XeTeX --disable-installer is permitted' + } + $report.engine=$engine + $report.version=(($banner -split '\r?\n') | Where-Object { $_ })[0] + $miktex=Get-Command miktex -ErrorAction SilentlyContinue + if(-not $miktex){throw 'MiKTeX read-only inventory utility is missing'} + function Inventory([string]$snapshot) { + $rows=@(& $miktex.Source packages list --template '{id}|{isInstalled}' 2>$null) + if($LASTEXITCODE -ne 0 -or $rows.Count -eq 0){throw 'MiKTeX inventory unavailable'} + $items=@($rows | ForEach-Object {[string]$_} | Sort-Object) + foreach($line in $items){ + if($line -cnotmatch '^[A-Za-z0-9][A-Za-z0-9._-]*\|(true|false)$'){ + throw 'MiKTeX inventory output is noncanonical' + } + } + $items | Set-Content -LiteralPath $snapshot -Encoding utf8 + return $items + } + $before=@(Inventory (Join-Path $OutputDir 'packages-before.txt')) + $work=Join-Path $OutputDir 'scratch' + New-Item -ItemType Directory -Force -Path $work | Out-Null + $original=Join-Path $root 'examples\cyrillic.tex' + $source=Join-Path $work 'cyrillic.tex' + $sourceHash=(Get-FileHash -LiteralPath $original -Algorithm SHA256).Hash + Copy-Item -LiteralPath $original -Destination $source -Force + $args=@('--disable-installer','-no-shell-escape','-halt-on-error', + '-interaction=nonstopmode','-file-line-error','-recorder', + "-output-directory=$work",$source) + Push-Location $work + try { + & $engine @args *> (Join-Path $OutputDir 'xelatex-output.log') + $texExit=$LASTEXITCODE + } finally {Pop-Location} + $report.tex_exit=$texExit + if($texExit -ne 0){throw "Real XeLaTeX failed with exit $texExit"} + $pdf=Join-Path $work 'cyrillic.pdf' + if(-not (Test-Path -LiteralPath $pdf -PathType Leaf)){throw 'No PDF produced'} + if((Get-FileHash $original -Algorithm SHA256).Hash -ne $sourceHash -or + (Get-FileHash $source -Algorithm SHA256).Hash -ne $sourceHash){ + throw 'Input source changed during compiler run' + } + $report.source_sha256=$sourceHash.ToLowerInvariant() + $report.pdf_sha256=(Get-FileHash $pdf -Algorithm SHA256).Hash.ToLowerInvariant() + $fonts=Get-Command pdffonts -ErrorAction SilentlyContinue + $info=Get-Command pdfinfo -ErrorAction SilentlyContinue + if($fonts -and $info){ + $details=(& $info.Source $pdf | Out-String) + $fontsText=(& $fonts.Source $pdf | Out-String) + $details | Set-Content (Join-Path $OutputDir 'pdfinfo.txt') + $fontsText | Set-Content (Join-Path $OutputDir 'pdffonts.txt') + if($details -notmatch '(?m)^Pages:\s+1\s*$' -or $fontsText -notmatch '\byes\b'){ + throw 'PDF page count or embedded font verification failed' + } + $report.font_audit='PASS' + } + $after=@(Inventory (Join-Path $OutputDir 'packages-after.txt')) + if($before.Count -ne $after.Count -or (Compare-Object $before $after)){ + throw 'Package inventory changed; offline gate is FAIL' + } + $report.packages_unchanged=$true + $report.package_count=$after.Count + if($CheckRust){ + if(-not (Get-Command cargo -ErrorAction SilentlyContinue)){ + $report.cargo='NOT_RUN_NO_CARGO' + }else{ + $env:CARGO_BUILD_JOBS='1' + $env:CARGO_INCREMENTAL='0' + & cargo test --locked -p tikzarium-core -p tikzarium-extensions -p tikzarium-app + if($LASTEXITCODE -ne 0){throw 'Cargo locked tests failed'} + $report.cargo='PASS_WINDOWS_SERVER_NOT_WIN10' + } + } + $report.status=if($report.font_audit -eq 'PASS'){ + 'PASS_USER_WINDOWS_SERVER_TEX_AND_PDF' + }else{'PARTIAL_PDF_FONTS_NOT_RUN'} + $exit=0 + }finally{Pop-Location} +}catch{ + $report.status='FAIL' + $report.error=$_.Exception.Message + Write-Error $report.error -ErrorAction Continue +}finally{ + $report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $reportPath -Encoding utf8 + Write-Host "Windows Server QA report: $reportPath" +} +exit $exit From 77ff306ee30d82d8325beb5b47165333a3761cc8 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:12:45 +0300 Subject: [PATCH 06/52] docs+ci(B42): manual user Windows Server MiKTeX workflow and studio UX acceptance --- .github/workflows/qa-selfhosted-winserver.yml | 42 ++++++++++ CHANGELOG.md | 11 ++- docs/STUDIO_UX_WINDOWS_QA.md | 81 +++++++++++++++++++ insights/2026-11-02_B42_UX_EVIDENCE.md | 22 +++++ ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 20 +++++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/qa-selfhosted-winserver.yml create mode 100644 docs/STUDIO_UX_WINDOWS_QA.md create mode 100644 insights/2026-11-02_B42_UX_EVIDENCE.md create mode 100644 plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md diff --git a/.github/workflows/qa-selfhosted-winserver.yml b/.github/workflows/qa-selfhosted-winserver.yml new file mode 100644 index 00000000..979b7f03 --- /dev/null +++ b/.github/workflows/qa-selfhosted-winserver.yml @@ -0,0 +1,42 @@ +name: Manual user Windows Server MiKTeX QA (not Win10) + +on: + workflow_dispatch: + inputs: + expected_main_sha: + description: "The exact reviewed main SHA; no PR code on persistent runner" + required: true + type: string + +permissions: + contents: read + +jobs: + user-windows-server: + if: github.ref == 'refs/heads/main' + runs-on: [self-hosted, Windows, X64, tikzarium-winserver] + timeout-minutes: 55 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Verify expected main SHA before any user-server execution + shell: pwsh + env: + EXPECTED_SHA: ${{ inputs.expected_main_sha }} + run: | + $actual = (git rev-parse HEAD).Trim() + if ($actual -cne $env:EXPECTED_SHA -or $actual -cne '${{ github.sha }}') { + throw 'Exact main SHA lease mismatch; refuse to run on user server' + } + - name: User MiKTeX offline proof and locked Rust test + shell: pwsh + run: | + ./scripts/tools/qa_windows_server_user.ps1 -OutputDir "$env:RUNNER_TEMP\tikzarium-user-miktex" -CheckRust + - name: Upload honest evidence including failure + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: tikzarium-user-winserver-${{ github.run_id }} + path: ${{ runner.temp }}/tikzarium-user-miktex/ + if-no-files-found: warn diff --git a/CHANGELOG.md b/CHANGELOG.md index f9280839..526f0a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ -## Unreleased — I60 independent B41 real-X11 inspector acceptance (PR #90; PDF preview/F1 recheck pending) +# Changelog + +## Unreleased — B42 redesigned Studio and dedicated user's MiKTeX Windows Server QA (PENDING) + +- Replaced repeated long tool buttons with native egui vector icons in four grouped floating palettes. No font-file asset or second PGF renderer; native C-catalogue commands and source-first semantics retained. +- Added a concise icon toolbar, permanently editable TeX beside the center drawing/PDF, independent Properties/Libraries/Help tabs, persistent workspace preferences and collapsed-by-default diagnostics. Classic UI remains temporarily available for regression comparisons. +- Added a manual main-SHA-gated GitHub Actions test for a dedicated user Windows Server runner and a no-auto-install MiKTeX/locked-Rust proof script. Runner connectivity and physical Windows10/M1/M4 are NOT_RUN; this is not GUI Session proof. +- Exact-head Rust/GUI/Windows CI and separate post-merge CI NOT_RUN at authoring. No release claim. + +d — I60 independent B41 real-X11 inspector acceptance (PR #90; PDF preview/F1 recheck pending) - Extend the same stacked I60 QA gate to re-run existing I46 dirty Open, I47 six held-pointer gestures, I53 positioning/help and I57 four-library real-X11 workflows on the **exact B41 app/src tree**, preserving fail-closed source/PDF/hash predicates and explicitly provisional B40-derived coordinates. Old head X11 failures remain NOT_RUN until new exact QA head plus manual artifact review. No production-code changes. - Add a separate exact-B41-source-tree Linux/X11 workflow exercising the **real egui window**: native Circle pointer → actual property TextEdit → unchanged source while draft pending → pinned C-backed Apply changing only selected command and metadata → native TextEdit Ctrl+Z history fence → visual Undo → Save/Open two distinguishable files including a CRLF/opaque Cyrillic source, followed by GUI external XeLaTeX PDF. Captures immutable sources, PDF, screenshots and per-file SHA-256; no production Rust/C or external TeX package installation changes. diff --git a/docs/STUDIO_UX_WINDOWS_QA.md b/docs/STUDIO_UX_WINDOWS_QA.md new file mode 100644 index 00000000..2fbe31ce --- /dev/null +++ b/docs/STUDIO_UX_WINDOWS_QA.md @@ -0,0 +1,81 @@ +# B42 — Studio UX and user's Windows Server QA + +## Owner feedback: icon groups, not walls of text buttons + +TikZarium is **not** Photoshop/Corel and does not implement PGF's renderer. +The user rejected its current dense egui toolbar and supplied prior UI design +mockups. This B42 implementation responds with: + +- Four **vector icon GROUPS** in a permanent 62pt left rail, one floating + palette at a time. Geometry contains line/rectangle/circle/Bezier; + connections contains arrow; annotation contains node. Extensions show + PGFPlots/CircuitikZ/tikz-cd as explicitly **not-yet-implemented** GUI + adapters, without hiding source-only package capabilities. +- Commands remain native C-catalogue TikZ generation with exactly the same + revision-checked Undo/SourceTarget and actual egui pointer handlers. + Icons are UI-only strokes, NOT a second geometry engine. +- An unobstructed editable TeX pane beside drawing/PDF, concise vector icon + Open/Save/Build/Export, grouped File/View, a right dock with separate + Properties/Libraries/Help tabs, persistent Light/Dark/Midnight/Paper and + hidden-by-default diagnostics. Figure fonts remain independent. +- Popover tooltips and F1 use the ONE Russian catalogue, not duplicated + descriptions or invented TikZ defaults. +- The old interface is temporarily retained under View for comparison. + +## User Windows Server 2025 with independently installed MiKTeX + +GitHub-hosted Windows Server 2022 ≠ user's persistent Windows Server 2025; +neither equals physical Windows 10 x64. The user says MiKTeX is installed, +but we have **not verified interactive access to the server or any self-hosted +runner registered with TikZarium**. + +Added `scripts/tools/qa_windows_server_user.ps1`: selects only external +MiKTeX, checks identity and `--disable-installer`, records read-only +package inventory before and after, compiles a **copy** of Cyrillic TeX, +compares source hash, archives real PDF, log, optional Poppler evidence and +optionally `cargo test --locked` with one build job. It installs/updates +nothing, never alters MiKTeX AutoInstall settings, and no engine fallback is +permitted. Its JSON distinguishes full font verification from PARTIAL. + +Manual GitHub workflow `qa-selfhosted-winserver.yml` requires a separately +registered non-admin runner with exact label `tikzarium-winserver`. It is +`workflow_dispatch` **only on main**, and refuses a mismatched expected +SHA. No pull_request-triggered persistent runner. Do not share GitHub runner +registration tokens in chat or put secrets in the repo. Ensure the public +repository cannot run arbitrary untrusted code on the self-hosted runner. +Restrict machine permissions and avoid using an Administrator runner. + +Recommended setup on the server, in PowerShell: + +```powershell +Get-Service | Where-Object Name -Like 'actions.runner*' +Get-Command xelatex,miktex,cargo,pdffonts,pdfinfo -ErrorAction SilentlyContinue | + Select-Object Name,Source +xelatex --disable-installer --version | Select-Object -First 2 +``` + +If a runner is currently attached to a different GitHub repository, do +not overwrite it. Set up another dedicated runner under GitHub +TikZarium → Settings → Actions → Runners → New self-hosted runner, and label +it `tikzarium-winserver`. Then after an accepted main merge, manually launch +the new workflow with exact main SHA. + +Running GitHub Actions as Windows *service* is Session 0 and **does not +constitute GUI acceptance**. A separate interactive RDP/VNC desktop must +test real window pointer/keyboard, Save/Reopen, F1, actual PDF viewport and +screen DPI. Mark this **NOT_RUN** until actual screenshots/logs exist. + +## Acceptance checkpoints + +1. Exact PR SHA: cargo fmt/test/clippy, hosted Linux/Windows/macOS CI. +2. New studio Linux/X11 desktop proof at 1366x768 and 850x560, screenshot + review; all 6 native tools, group picker, C help, source, Undo, libraries, + and externally produced PDF. Old coordinate-based B41 X11 runs do not + automatically count, and must fail closed if the app tree changes. +3. Self-hosted Windows Server MiKTeX code/fixture PDF test, real artifact. +4. Separate physical Win10 x64 and macOS M1/M4 acceptance (NOT_RUN). +5. No announcement of release from a concept render or a hosted server pass. + +Current status on authoring: B42 source written; exact-head CI NOT_RUN, +new real B42 GUI NOT_RUN, user Windows Server runner NOT_CONNECTED/NOT_RUN, +physical Windows 10 and Mac M1/M4 NOT_RUN. diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md new file mode 100644 index 00000000..e9996bbc --- /dev/null +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -0,0 +1,22 @@ +# B42 forensic UX review and QA honesty + +Observed B41 real X11 images in UI album: +- long text Open/Save/Build/Export/Discovery menus occupy two rows; +- six full-word tool buttons occupy another row above provisional guide; +- combined Help + Library + Inspector + Package inventory is a long + inaccessible scroll; bottom log uses 145px even without an error; +- compact Fit Page had to shrink PDF to 7%. + +Selected revised architecture: only 4 group icons remain in rail; grouped +floating palettes avoid permanently taking PDF/source width. 4 command +icons on top; File/View only as secondary menus; context right dock tabs. +Existing source-patch and external compiler contracts unchanged. + +QA status at authoring: source code created; no local Rust binary +in integrated container, NOT_RUN local; CI on exact PR HEAD pending; +new real egui X11 NOT_RUN until artifacts reviewed. User-installed MiKTeX +reported by owner but GitHub runner connectivity NOT_VERIFIED; +persistent Windows Server test NOT_RUN; physical Win10/M1/M4 NOT_RUN. + +Risk: old coordinate-pinned desktop drivers must be recalibrated for +new layout, not marked PASS from historical B41. Keep existing CI gates. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md new file mode 100644 index 00000000..e2a257e7 --- /dev/null +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -0,0 +1,20 @@ +# B42 — Large ergonomic studio block and Windows Server test bridge + +Source of truth: i-shutov/tikzarium main +93db2044b5b91f79c0e774be8806a3693ba7329b; +AGENTS, current source and user's previous interface mockups. + +Owner rejects many text buttons and poor compact-window readability. +Deliver native vector icons grouped into popover palettes, short action +toolbar, persistent code beside drawing/PDF, independently scrolling +context tabs and on-demand diagnostics. Keep original TikZ and XeLaTeX. + +Tools grouped: geometry 4, connections 1, annotation 1, extensions +explicit NOT_IMPLEMENTED for adapters. No duplicate C catalogue. +Undo/Save/Open and CRLF remain untouched. Legacy GUI temporary fallback. + +Windows Server stream: safe manual workflow with own user-installed MiKTeX, +no hidden installer, actual PDF inventory/hash, cargo --locked; self-hosted +runner is not known connected yet. Windows Server != Windows10. +B42 exact-head hosted CI + real X11 screenshots and user-server external +test are release gates, never infer them from source changes alone. From 8b4fea3bdef639baf59813f564111c7cfebc0b6a Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:17:34 +0300 Subject: [PATCH 07/52] style(B42): apply exact hosted rustfmt diffs across Studio, vector icons and existing guide --- crates/tikzarium-app/src/main.rs | 414 ++++++++++++++---------- crates/tikzarium-app/src/studio_ui.rs | 336 ++++++++++++------- crates/tikzarium-app/src/tikz_canvas.rs | 49 +-- crates/tikzarium-app/src/ui_theme.rs | 3 +- 4 files changed, 490 insertions(+), 312 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index f1ed2704..496645ca 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1114,30 +1114,56 @@ impl TikzariumApp { ui.horizontal(|ui| { ui.strong(egui::RichText::new("TikZarium").size(20.0)); ui.separator(); - if studio_ui::icon_button( - ui, StudioIcon::Open, false, 34.0, "Открыть TeX · Ctrl+O" - ).clicked() && ready { + if studio_ui::icon_button(ui, StudioIcon::Open, false, 34.0, "Открыть TeX · Ctrl+O") + .clicked() + && ready + { self.open_document_dialog(); } if studio_ui::icon_button( - ui, StudioIcon::Save, false, 34.0, "Сохранить исходник · Ctrl+S" - ).clicked() && ready { + ui, + StudioIcon::Save, + false, + 34.0, + "Сохранить исходник · Ctrl+S", + ) + .clicked() + && ready + { self.save(); } if studio_ui::icon_button( - ui, StudioIcon::Build, self.receiver.is_some(), 34.0, - "Собрать точный PDF через внешний XeLaTeX · Ctrl+Enter" - ).clicked() && ready { + ui, + StudioIcon::Build, + self.receiver.is_some(), + 34.0, + "Собрать точный PDF через внешний XeLaTeX · Ctrl+Enter", + ) + .clicked() + && ready + { self.build(); } if studio_ui::icon_button( - ui, StudioIcon::Export, false, 34.0, - "Экспорт PDF. Только актуальный результат XeLaTeX." - ).clicked() && self.pdf.is_some() && !self.preview_stale() { + ui, + StudioIcon::Export, + false, + 34.0, + "Экспорт PDF. Только актуальный результат XeLaTeX.", + ) + .clicked() + && self.pdf.is_some() + && !self.preview_stale() + { self.export(); } - if self.receiver.is_some() && ui.small_button("■").on_hover_text("Отменить сборку").clicked() - && let Some(cancel) = &self.cancellation { + if self.receiver.is_some() + && ui + .small_button("■") + .on_hover_text("Отменить сборку") + .clicked() + && let Some(cancel) = &self.cancellation + { cancel.cancel(); } ui.separator(); @@ -1148,11 +1174,17 @@ impl TikzariumApp { .desired_width(260.0) .hint_text("figure.tex"), ); - if ui.add_enabled(ready, egui::Button::new("Открыть путь")).clicked() { + if ui + .add_enabled(ready, egui::Button::new("Открыть путь")) + .clicked() + { self.open_path(PathBuf::from(self.file_field.trim())); ui.close(); } - if ui.add_enabled(ready, egui::Button::new("Сохранить как…")).clicked() { + if ui + .add_enabled(ready, egui::Button::new("Сохранить как…")) + .clicked() + { self.save_as(); ui.close(); } @@ -1173,17 +1205,27 @@ impl TikzariumApp { } }); if studio_ui::icon_button( - ui, StudioIcon::Help, self.prefs.studio_dock == StudioDock::Help, 32.0, - "Контекстная справка F1 из общего TikZ-каталога" - ).clicked() { + ui, + StudioIcon::Help, + self.prefs.studio_dock == StudioDock::Help, + 32.0, + "Контекстная справка F1 из общего TikZ-каталога", + ) + .clicked() + { self.authoring.help = true; self.prefs.studio_dock = StudioDock::Help; self.prefs.studio_inspector_open = true; } if studio_ui::icon_button( - ui, StudioIcon::Console, self.prefs.studio_console_open, 32.0, - "Показать или скрыть диагностику и журнал XeLaTeX" - ).clicked() { + ui, + StudioIcon::Console, + self.prefs.studio_console_open, + 32.0, + "Показать или скрыть диагностику и журнал XeLaTeX", + ) + .clicked() + { self.prefs.studio_console_open = !self.prefs.studio_console_open; } }); @@ -1194,7 +1236,10 @@ impl TikzariumApp { self.fit_page_active = true; } ui.separator(); - let filename = self.document.path.as_ref() + let filename = self + .document + .path + .as_ref() .map_or_else(|| "Новый .tex".to_owned(), |p| p.display().to_string()); ui.add(egui::Label::new(filename).truncate()); if self.document.dirty() { @@ -1218,62 +1263,78 @@ impl TikzariumApp { ui.separator(); let prior = self.authoring.source_revision(); if let Some(line) = tikz_canvas::show_inspector_action_bar( - ui, &mut self.authoring, &mut self.document.source + ui, + &mut self.authoring, + &mut self.document.source, ) { self.jump_to_line = Some(line); self.highlight_generated_line = true; } fence_source_editor_undo_on_revision_change( - prior, &self.authoring, &mut self.reset_source_editor_undo_history + prior, + &self.authoring, + &mut self.reset_source_editor_undo_history, ); ui.separator(); egui::ScrollArea::vertical() .auto_shrink([false, false]) - .show(ui, |ui| { - match self.prefs.studio_dock { - StudioDock::Properties => { - let prior = self.authoring.source_revision(); - let caps = self.primitive_capabilities(); - if let Some(line) = tikz_canvas::show_inspector( - ui, &mut self.authoring, &mut self.document.source, caps - ) { - self.jump_to_line = Some(line); - self.highlight_generated_line = true; - } - fence_source_editor_undo_on_revision_change( - prior, &self.authoring, &mut self.reset_source_editor_undo_history - ); - } - StudioDock::Libraries => { - ui.heading("Библиотеки проекта"); - ui.small("Подключение — показанная обратимая правка преамбулы; не установка."); - let prior = self.authoring.source_revision(); - if let Some(line) = tikz_libraries::show_library_panel( - ui, &mut self.authoring, &mut self.document.source, - self.tex.xelatex.is_some() - ) { - self.jump_to_line = Some(line); - self.highlight_generated_line = true; - } - fence_source_editor_undo_on_revision_change( - prior, &self.authoring, &mut self.reset_source_editor_undo_history - ); - ui.collapsing("Реестр доступности пакетов", |ui| { - for item in &self.package_statuses { - ui.strong(&item.manifest.display_name); - ui.small(format!( - "Установка: {:?} · XeLaTeX: {:?} · GUI-адаптер: {}", - item.installation, item.compilation, - if item.visual_editable { "да" } else { "нет" } - )); - } - }); + .show(ui, |ui| match self.prefs.studio_dock { + StudioDock::Properties => { + let prior = self.authoring.source_revision(); + let caps = self.primitive_capabilities(); + if let Some(line) = tikz_canvas::show_inspector( + ui, + &mut self.authoring, + &mut self.document.source, + caps, + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; } - StudioDock::Help => { - tikz_libraries::show_help_search(ui, &mut self.help_query); - ui.separator(); - studio_ui::show_context_help(ui, &self.authoring); + fence_source_editor_undo_on_revision_change( + prior, + &self.authoring, + &mut self.reset_source_editor_undo_history, + ); + } + StudioDock::Libraries => { + ui.heading("Библиотеки проекта"); + ui.small("Подключение — показанная обратимая правка преамбулы; не установка."); + let prior = self.authoring.source_revision(); + if let Some(line) = tikz_libraries::show_library_panel( + ui, + &mut self.authoring, + &mut self.document.source, + self.tex.xelatex.is_some(), + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; } + fence_source_editor_undo_on_revision_change( + prior, + &self.authoring, + &mut self.reset_source_editor_undo_history, + ); + ui.collapsing("Реестр доступности пакетов", |ui| { + for item in &self.package_statuses { + ui.strong(&item.manifest.display_name); + ui.small(format!( + "Установка: {:?} · XeLaTeX: {:?} · GUI-адаптер: {}", + item.installation, + item.compilation, + if item.visual_editable { + "да" + } else { + "нет" + } + )); + } + }); + } + StudioDock::Help => { + tikz_libraries::show_help_search(ui, &mut self.help_query); + ui.separator(); + studio_ui::show_context_help(ui, &self.authoring); } }); } @@ -1371,12 +1432,13 @@ impl TikzariumApp { let previous_revision = self.authoring.source_revision(); let inserted = if self.prefs.studio_mode { tikz_canvas::show_guide_studio( - ui, &mut self.authoring, &mut self.document.source, caps + ui, + &mut self.authoring, + &mut self.document.source, + caps, ) } else { - tikz_canvas::show_guide( - ui, &mut self.authoring, &mut self.document.source, caps - ) + tikz_canvas::show_guide(ui, &mut self.authoring, &mut self.document.source, caps) }; if let Some(line) = inserted { self.jump_to_line = Some(line); @@ -1669,96 +1731,97 @@ impl eframe::App for TikzariumApp { self.workspace = WorkspaceMode::Split; self.show_studio_toolbar(ctx); } else { - egui::TopBottomPanel::top("tools").show(ctx, |ui| { - if self.pending_open.is_some() { - ui.disable(); - } - ui.horizontal_wrapped(|ui| { - ui.heading("TikZarium"); - if ui - .add_enabled(file_actions_enabled, egui::Button::new("Open")) - .on_hover_text("Ctrl+O / Cmd+O") - .clicked() - { - self.open_document_dialog(); - } - ui.label("Path:"); - ui.add( - egui::TextEdit::singleline(&mut self.file_field) - .desired_width(280.0) - .hint_text("path/to/figure.tex"), - ); - if ui - .add_enabled(file_actions_enabled, egui::Button::new("Open path")) - .clicked() - { - self.open_path(PathBuf::from(self.file_field.trim())); - } - if ui - .add_enabled(file_actions_enabled, egui::Button::new("Save")) - .on_hover_text("Ctrl+S / Cmd+S") - .clicked() - { - self.save(); - } - if ui - .add_enabled(file_actions_enabled, egui::Button::new("Save As…")) - .on_hover_text("Ctrl+Shift+S / Cmd+Shift+S") - .clicked() - { - self.save_as(); - } - if ui - .add_enabled(file_actions_enabled, egui::Button::new("Build (XeLaTeX)")) - .on_hover_text("Ctrl+Enter / Cmd+Enter") - .clicked() - { - self.build(); - } - if ui - .add_enabled(self.receiver.is_some(), egui::Button::new("Cancel")) - .clicked() - && let Some(cancel) = &self.cancellation - { - cancel.cancel(); - } - if ui - .add_enabled( - self.pdf.is_some() && !self.preview_stale(), - egui::Button::new("Export PDF"), - ) - .clicked() - { - self.export(); - } - if ui - .add_enabled(self.receiver.is_none(), egui::Button::new("Discover TeX")) - .clicked() - { - self.refresh_tex(); - } - }); - ui.horizontal_wrapped(|ui| { - ui.label("Workspace:"); - ui.selectable_value(&mut self.workspace, WorkspaceMode::Source, "Source"); - ui.selectable_value(&mut self.workspace, WorkspaceMode::Split, "Split"); - ui.selectable_value(&mut self.workspace, WorkspaceMode::Canvas, "Canvas"); - ui.separator(); - self.prefs.draw_toolbar(ui, ctx); - if ui.button("Figure fonts").clicked() { - self.prefs.show_font_lab = !self.prefs.show_font_lab; - } - ui.checkbox(&mut self.show_packages, "Library inventory"); - ui.checkbox(&mut self.show_log, "Diagnostics / log"); - if self.document.dirty() { - ui.colored_label(egui::Color32::YELLOW, "Unsaved edits"); - } - if self.preview_stale() { - ui.colored_label(egui::Color32::YELLOW, "PDF stale"); + egui::TopBottomPanel::top("tools").show(ctx, |ui| { + if self.pending_open.is_some() { + ui.disable(); } - ui.label(&self.status); + ui.horizontal_wrapped(|ui| { + ui.heading("TikZarium"); + if ui + .add_enabled(file_actions_enabled, egui::Button::new("Open")) + .on_hover_text("Ctrl+O / Cmd+O") + .clicked() + { + self.open_document_dialog(); + } + ui.label("Path:"); + ui.add( + egui::TextEdit::singleline(&mut self.file_field) + .desired_width(280.0) + .hint_text("path/to/figure.tex"), + ); + if ui + .add_enabled(file_actions_enabled, egui::Button::new("Open path")) + .clicked() + { + self.open_path(PathBuf::from(self.file_field.trim())); + } + if ui + .add_enabled(file_actions_enabled, egui::Button::new("Save")) + .on_hover_text("Ctrl+S / Cmd+S") + .clicked() + { + self.save(); + } + if ui + .add_enabled(file_actions_enabled, egui::Button::new("Save As…")) + .on_hover_text("Ctrl+Shift+S / Cmd+Shift+S") + .clicked() + { + self.save_as(); + } + if ui + .add_enabled(file_actions_enabled, egui::Button::new("Build (XeLaTeX)")) + .on_hover_text("Ctrl+Enter / Cmd+Enter") + .clicked() + { + self.build(); + } + if ui + .add_enabled(self.receiver.is_some(), egui::Button::new("Cancel")) + .clicked() + && let Some(cancel) = &self.cancellation + { + cancel.cancel(); + } + if ui + .add_enabled( + self.pdf.is_some() && !self.preview_stale(), + egui::Button::new("Export PDF"), + ) + .clicked() + { + self.export(); + } + if ui + .add_enabled(self.receiver.is_none(), egui::Button::new("Discover TeX")) + .clicked() + { + self.refresh_tex(); + } + }); + ui.horizontal_wrapped(|ui| { + ui.label("Workspace:"); + ui.selectable_value(&mut self.workspace, WorkspaceMode::Source, "Source"); + ui.selectable_value(&mut self.workspace, WorkspaceMode::Split, "Split"); + ui.selectable_value(&mut self.workspace, WorkspaceMode::Canvas, "Canvas"); + ui.separator(); + self.prefs.draw_toolbar(ui, ctx); + if ui.button("Figure fonts").clicked() { + self.prefs.show_font_lab = !self.prefs.show_font_lab; + } + ui.checkbox(&mut self.show_packages, "Library inventory"); + ui.checkbox(&mut self.show_log, "Diagnostics / log"); + if self.document.dirty() { + ui.colored_label(egui::Color32::YELLOW, "Unsaved edits"); + } + if self.preview_stale() { + ui.colored_label(egui::Color32::YELLOW, "PDF stale"); + } + ui.label(&self.status); + }); }); - }); + } let split_layout = SplitPanelLayout::for_width(ctx.available_rect().width()); if self.prefs.studio_mode && self.prefs.studio_inspector_open { @@ -1769,7 +1832,9 @@ impl eframe::App for TikzariumApp { .min_width(225.0) .max_width(studio.inspector_max) .show(ctx, |ui| { - if self.pending_open.is_some() { ui.disable(); } + if self.pending_open.is_some() { + ui.disable(); + } self.show_studio_inspector(ui); }); } @@ -1799,7 +1864,8 @@ impl eframe::App for TikzariumApp { }); } if (!self.prefs.studio_mode && self.show_log) - || (self.prefs.studio_mode && self.prefs.studio_console_open) { + || (self.prefs.studio_mode && self.prefs.studio_console_open) + { egui::TopBottomPanel::bottom("log") .resizable(true) .default_height(145.0) @@ -1874,15 +1940,23 @@ impl eframe::App for TikzariumApp { }); }); } - if self.prefs.studio_mode && self.workspace == WorkspaceMode::Split - && self.prefs.studio_tool_rail { + if self.prefs.studio_mode + && self.workspace == WorkspaceMode::Split + && self.prefs.studio_tool_rail + { egui::SidePanel::left("b42-tool-groups") .resizable(false) - .exact_width(studio_ui::StudioLayout::for_width(ctx.available_rect().width()).tool_rail_width) + .exact_width( + studio_ui::StudioLayout::for_width(ctx.available_rect().width()) + .tool_rail_width, + ) .show(ctx, |ui| { - if self.pending_open.is_some() { ui.disable(); } + if self.pending_open.is_some() { + ui.disable(); + } if let Some(anchor) = studio_ui::show_group_rail( - ui, &mut self.studio_group, + ui, + &mut self.studio_group, studio_ui::StudioLayout::for_width(ctx.available_rect().width()).narrow, ) { self.studio_palette_anchor = anchor; @@ -1907,8 +1981,12 @@ impl eframe::App for TikzariumApp { if self.prefs.studio_mode && self.pending_open.is_none() { let mut open_libraries = false; studio_ui::show_palette( - ctx, &mut self.studio_group, self.studio_palette_anchor, - &mut self.authoring, &self.document.source, &mut open_libraries, + ctx, + &mut self.studio_group, + self.studio_palette_anchor, + &mut self.authoring, + &self.document.source, + &mut open_libraries, ); if open_libraries { self.prefs.studio_dock = StudioDock::Libraries; diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 0e084cfa..f93e445b 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -1,10 +1,11 @@ //! B42: grouped icon palettes for TikZ-native authoring. //! Tool and parameter semantics belong to the existing C catalogue. Vector //! icon strokes are only UI affordances, never a second PGF renderer. +use crate::tikz_authoring::{self, Session, Tool}; use eframe::egui::{self, Color32, Pos2, Rect, Sense, Shape, Stroke, Vec2}; use serde::{Deserialize, Serialize}; use tikzarium_extensions::primitives::Evidence; -use crate::tikz_authoring::{self, Session, Tool}; + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum StudioDock { @@ -64,7 +65,11 @@ pub struct StudioLayout { } impl StudioLayout { pub fn for_width(width: f32) -> Self { - let width = if width.is_finite() { width.max(0.0) } else { 0.0 }; + let width = if width.is_finite() { + width.max(0.0) + } else { + 0.0 + }; Self { tool_rail_width: 62.0, inspector_width: (width * 0.22).clamp(225.0, 300.0), @@ -92,11 +97,14 @@ fn draw_icon(ui: &egui::Ui, rect: Rect, kind: StudioIcon, selected: bool) { let p = ui.painter_at(rect); let c = rect.center(); let t = rect.width().min(rect.height()) * 0.28; - let stroke = Stroke::new(1.9, if selected { - ui.visuals().selection.stroke.color - } else { - ui.visuals().text_color() - }); + let stroke = Stroke::new( + 1.9, + if selected { + ui.visuals().selection.stroke.color + } else { + ui.visuals().text_color() + }, + ); let point = |x: f32, y: f32| Pos2::new(c.x + t * x, c.y + t * y); match kind { StudioIcon::Tool(Tool::Line) => { @@ -110,97 +118,148 @@ fn draw_icon(ui: &egui::Ui, rect: Rect, kind: StudioIcon, selected: bool) { line(&p, point(1.0, 0.0), point(0.3, 0.65), stroke); } StudioIcon::Tool(Tool::Rectangle) => { - let a=point(-1.0,-0.72); let b=point(1.0,0.72); - p.add(Shape::closed_line(vec![ - a, point(1.0,-0.72), b, point(-1.0,0.72) - ],stroke)); + let a = point(-1.0, -0.72); + let b = point(1.0, 0.72); + p.add(Shape::closed_line( + vec![a, point(1.0, -0.72), b, point(-1.0, 0.72)], + stroke, + )); } StudioIcon::Tool(Tool::Circle) => { - p.circle_stroke(c, t*0.9, stroke); + p.circle_stroke(c, t * 0.9, stroke); } StudioIcon::Tool(Tool::Node) => { - p.add(Shape::closed_line(vec![ - point(-1.0,-0.8),point(1.0,-0.8), - point(1.0,0.8),point(-1.0,0.8) - ], stroke)); - p.text(c,egui::Align2::CENTER_CENTER,"T", - egui::FontId::proportional(t*1.15),stroke.color); + p.add(Shape::closed_line( + vec![ + point(-1.0, -0.8), + point(1.0, -0.8), + point(1.0, 0.8), + point(-1.0, 0.8), + ], + stroke, + )); + p.text( + c, + egui::Align2::CENTER_CENTER, + "T", + egui::FontId::proportional(t * 1.15), + stroke.color, + ); } StudioIcon::Tool(Tool::Cubic) => { - let mut points=Vec::with_capacity(21); + let mut points = Vec::with_capacity(21); for i in 0..=20 { - let v=i as f32/20.0;let inv=1.0-v; - let x=inv*inv*inv*(-1.1)+3.0*inv*inv*v*(-0.45) - +3.0*inv*v*v*(0.45)+v*v*v*(1.1); - let y=inv*inv*inv*(0.8)+3.0*inv*inv*v*(-1.3) - +3.0*inv*v*v*(1.3)+v*v*v*(-0.8); - points.push(point(x,y)); + let v = i as f32 / 20.0; + let inv = 1.0 - v; + let x = inv * inv * inv * (-1.1) + + 3.0 * inv * inv * v * (-0.45) + + 3.0 * inv * v * v * (0.45) + + v * v * v * (1.1); + let y = inv * inv * inv * (0.8) + + 3.0 * inv * inv * v * (-1.3) + + 3.0 * inv * v * v * (1.3) + + v * v * v * (-0.8); + points.push(point(x, y)); } - p.add(Shape::line(points,stroke)); + p.add(Shape::line(points, stroke)); } StudioIcon::Group(StudioGroup::Geometry) => { - p.circle_stroke(point(-0.45,-0.4),t*0.60,stroke); - p.add(Shape::closed_line(vec![ - point(0.10,-0.10),point(1.0,-0.10), - point(1.0,0.82),point(0.10,0.82) - ],stroke)); + p.circle_stroke(point(-0.45, -0.4), t * 0.60, stroke); + p.add(Shape::closed_line( + vec![ + point(0.10, -0.10), + point(1.0, -0.10), + point(1.0, 0.82), + point(0.10, 0.82), + ], + stroke, + )); + } + StudioIcon::Group(StudioGroup::Connections) => { + draw_icon(ui, rect, StudioIcon::Tool(Tool::Arrow), selected) + } + StudioIcon::Group(StudioGroup::Text) => { + draw_icon(ui, rect, StudioIcon::Tool(Tool::Node), selected) } - StudioIcon::Group(StudioGroup::Connections) => - draw_icon(ui,rect,StudioIcon::Tool(Tool::Arrow),selected), - StudioIcon::Group(StudioGroup::Text) => - draw_icon(ui,rect,StudioIcon::Tool(Tool::Node),selected), StudioIcon::Group(StudioGroup::Extensions) => { - for (x,y) in [(-0.65,-0.65),(0.65,-0.65),(-0.65,0.65),(0.65,0.65)] { - p.circle_filled(point(x,y),2.6,stroke.color); + for (x, y) in [(-0.65, -0.65), (0.65, -0.65), (-0.65, 0.65), (0.65, 0.65)] { + p.circle_filled(point(x, y), 2.6, stroke.color); } - line(&p,point(-0.65,-0.65),point(0.65,0.65),stroke); - line(&p,point(-0.65,0.65),point(0.65,-0.65),stroke); + line(&p, point(-0.65, -0.65), point(0.65, 0.65), stroke); + line(&p, point(-0.65, 0.65), point(0.65, -0.65), stroke); } StudioIcon::Open => { - p.add(Shape::line(vec![ - point(-1.05,-0.45),point(-0.45,-0.45), - point(-0.18,-0.15),point(0.95,-0.15), - point(0.85,0.76),point(-0.98,0.76), - point(-1.05,-0.45) - ],stroke)); + p.add(Shape::line( + vec![ + point(-1.05, -0.45), + point(-0.45, -0.45), + point(-0.18, -0.15), + point(0.95, -0.15), + point(0.85, 0.76), + point(-0.98, 0.76), + point(-1.05, -0.45), + ], + stroke, + )); } StudioIcon::Save => { - p.add(Shape::closed_line(vec![ - point(-0.85,-1.0),point(0.58,-1.0), - point(0.95,-0.65),point(0.95,1.0), - point(-0.85,1.0) - ],stroke)); - line(&p,point(-0.43,-0.9),point(-0.43,-0.3),stroke); - line(&p,point(-0.43,-0.3),point(0.38,-0.3),stroke); - line(&p,point(-0.43,0.25),point(0.55,0.25),stroke); + p.add(Shape::closed_line( + vec![ + point(-0.85, -1.0), + point(0.58, -1.0), + point(0.95, -0.65), + point(0.95, 1.0), + point(-0.85, 1.0), + ], + stroke, + )); + line(&p, point(-0.43, -0.9), point(-0.43, -0.3), stroke); + line(&p, point(-0.43, -0.3), point(0.38, -0.3), stroke); + line(&p, point(-0.43, 0.25), point(0.55, 0.25), stroke); } StudioIcon::Build => { - p.add(Shape::closed_line(vec![ - point(-0.65,-1.0),point(1.0,0.0),point(-0.65,1.0) - ],stroke)); + p.add(Shape::closed_line( + vec![point(-0.65, -1.0), point(1.0, 0.0), point(-0.65, 1.0)], + stroke, + )); } StudioIcon::Export => { - p.add(Shape::line(vec![ - point(-0.9,-0.3),point(-0.9,0.98), - point(0.9,0.98),point(0.9,-0.3) - ],stroke)); - line(&p,point(0.0,-1.0),point(0.0,0.38),stroke); - line(&p,point(0.0,0.38),point(-0.5,-0.13),stroke); - line(&p,point(0.0,0.38),point(0.5,-0.13),stroke); + p.add(Shape::line( + vec![ + point(-0.9, -0.3), + point(-0.9, 0.98), + point(0.9, 0.98), + point(0.9, -0.3), + ], + stroke, + )); + line(&p, point(0.0, -1.0), point(0.0, 0.38), stroke); + line(&p, point(0.0, 0.38), point(-0.5, -0.13), stroke); + line(&p, point(0.0, 0.38), point(0.5, -0.13), stroke); } StudioIcon::Help => { - p.circle_stroke(c,t,stroke); - p.text(c,egui::Align2::CENTER_CENTER,"?", - egui::FontId::proportional(t*1.6),stroke.color); + p.circle_stroke(c, t, stroke); + p.text( + c, + egui::Align2::CENTER_CENTER, + "?", + egui::FontId::proportional(t * 1.6), + stroke.color, + ); } StudioIcon::Console => { - p.add(Shape::closed_line(vec![ - point(-1.0,-0.8),point(1.0,-0.8), - point(1.0,0.8),point(-1.0,0.8) - ],stroke)); - line(&p,point(-0.65,-0.3),point(-0.2,0.0),stroke); - line(&p,point(-0.2,0.0),point(-0.65,0.3),stroke); - line(&p,point(-0.05,0.35),point(0.65,0.35),stroke); + p.add(Shape::closed_line( + vec![ + point(-1.0, -0.8), + point(1.0, -0.8), + point(1.0, 0.8), + point(-1.0, 0.8), + ], + stroke, + )); + line(&p, point(-0.65, -0.3), point(-0.2, 0.0), stroke); + line(&p, point(-0.2, 0.0), point(-0.65, 0.3), stroke); + line(&p, point(-0.05, 0.35), point(0.65, 0.35), stroke); } } } @@ -214,18 +273,18 @@ pub fn icon_button( side: f32, tooltip: &str, ) -> egui::Response { - let (rect,response)=ui.allocate_exact_size(Vec2::splat(side),Sense::click()); + let (rect, response) = ui.allocate_exact_size(Vec2::splat(side), Sense::click()); if ui.is_rect_visible(rect) { - let p=ui.painter_at(rect); - let fill=if selected { + let p = ui.painter_at(rect); + let fill = if selected { ui.visuals().selection.bg_fill } else if response.hovered() { ui.visuals().widgets.hovered.bg_fill } else { ui.visuals().widgets.inactive.bg_fill }; - p.rect_filled(rect,egui::CornerRadius::same(7),fill); - draw_icon(ui,rect,kind,selected); + p.rect_filled(rect, egui::CornerRadius::same(7), fill); + draw_icon(ui, rect, kind, selected); } response.on_hover_text(tooltip) } @@ -237,18 +296,28 @@ pub fn show_group_rail( active: &mut Option, narrow: bool, ) -> Option { - let _=narrow; - ui.vertical_centered(|ui|{ + let _ = narrow; + ui.vertical_centered(|ui| { ui.strong("TikZ"); ui.separator(); }); - let mut anchor=None; + let mut anchor = None; for group in StudioGroup::ALL { - let tooltip=format!("Палитра: {}. Нажмите для раскрытия.",group.label()); - let response=icon_button(ui,StudioIcon::Group(group),*active==Some(group),43.0,&tooltip); + let tooltip = format!("Палитра: {}. Нажмите для раскрытия.", group.label()); + let response = icon_button( + ui, + StudioIcon::Group(group), + *active == Some(group), + 43.0, + &tooltip, + ); if response.clicked() { - *active=if *active==Some(group){None}else{Some(group)}; - anchor=Some(response.rect.right_top()); + *active = if *active == Some(group) { + None + } else { + Some(group) + }; + anchor = Some(response.rect.right_top()); } } anchor @@ -264,10 +333,10 @@ pub fn show_palette( source: &str, open_libraries: &mut bool, ) { - if ctx.input(|input| input.key_pressed(egui::Key::Escape)){ - *group=None; + if ctx.input(|input| input.key_pressed(egui::Key::Escape)) { + *group = None; } - let Some(selected)=*group else {return}; + let Some(selected) = *group else { return }; egui::Area::new(egui::Id::new("b42-group-palette")) .order(egui::Order::Foreground) .fixed_pos(anchor+egui::vec2(6.0,-36.0)) @@ -318,42 +387,53 @@ pub fn show_palette( } /// The same read-only C catalogue consumed by the inspector and F1. -pub fn show_context_help(ui:&mut egui::Ui,session:&Session) { +pub fn show_context_help(ui: &mut egui::Ui, session: &Session) { ui.heading("Справка TikZ"); - if let Some(tool)=session.active { - let info=tikz_authoring::spec(tool); + if let Some(tool) = session.active { + let info = tikz_authoring::spec(tool); ui.strong(&info.title_ru); ui.label(&info.help_ru); ui.code(&info.syntax); ui.label("Пример:"); ui.code(&info.example); - ui.hyperlink_to(format!("PGF/TikZ §{}",info.manual_section),&info.manual_url); + ui.hyperlink_to( + format!("PGF/TikZ §{}", info.manual_section), + &info.manual_url, + ); ui.small(format!( "Пакет: {} · Библиотеки: {} · PGF {}", info.requirements.packages.join(", "), info.requirements.libraries.join(", "), info.requirements.documented_against )); - for param in &info.parameter{ - egui::CollapsingHeader::new(¶m.id).show(ui,|ui|{ + for param in &info.parameter { + egui::CollapsingHeader::new(¶m.id).show(ui, |ui| { ui.label(¶m.help_ru); ui.code(¶m.syntax); ui.small(format!( "Тип: {} · Единицы: {} · Допустимые значения: {}", - param.kind,param.units.join(", "),param.allowed_values.join(", ") + param.kind, + param.units.join(", "), + param.allowed_values.join(", ") )); - if param.default_provenance=="editor_preset" { + if param.default_provenance == "editor_preset" { ui.small(format!( - "Предустановка редактора: {} (НЕ default TikZ)",param.default + "Предустановка редактора: {} (НЕ default TikZ)", + param.default )); - }else{ui.small("Default TikZ: UNKNOWN");} + } else { + ui.small("Default TikZ: UNKNOWN"); + } ui.label(¶m.warning_ru); ui.label(¶m.interaction_ru); ui.code(¶m.example); - ui.hyperlink_to(format!("PGF/TikZ §{}",param.manual_section),¶m.manual_url); + ui.hyperlink_to( + format!("PGF/TikZ §{}", param.manual_section), + ¶m.manual_url, + ); }); } - }else{ + } else { ui.label("Выберите инструмент в левой палитре."); ui.small("Подсказки и параметры поступают из единого каталога C."); } @@ -364,41 +444,57 @@ mod tests { use super::*; #[test] fn compact_group_rail_is_bounded_with_expansion_as_floating_overlay() { - for width in [850.0_f32,1180.0,1366.0,1920.0] { - let layout=StudioLayout::for_width(width); - assert_eq!(layout.tool_rail_width,62.0); - assert!(width-layout.tool_rail_width-layout.inspector_width-240.0>190.0); + for width in [850.0_f32, 1180.0, 1366.0, 1920.0] { + let layout = StudioLayout::for_width(width); + assert_eq!(layout.tool_rail_width, 62.0); + assert!(width - layout.tool_rail_width - layout.inspector_width - 240.0 > 190.0); } } #[test] fn groups_cover_all_native_c_tools_once_and_expose_future_adapters() { - let all=StudioGroup::ALL.iter().flat_map(|g|g.tools()).copied() + let all = StudioGroup::ALL + .iter() + .flat_map(|g| g.tools()) + .copied() .collect::>(); - assert_eq!(all.len(),6); - for item in crate::tikz_authoring::TOOLS{ - assert_eq!(all.iter().filter(|&&t|t==item).count(),1); - assert!(tikz_authoring::spec(item).manual_url.starts_with("https://tikz.dev/")); + assert_eq!(all.len(), 6); + for item in crate::tikz_authoring::TOOLS { + assert_eq!(all.iter().filter(|&&t| t == item).count(), 1); + assert!( + tikz_authoring::spec(item) + .manual_url + .starts_with("https://tikz.dev/") + ); } assert!(StudioGroup::Extensions.tools().is_empty()); } #[test] fn icon_tooltips_and_dock_types_do_not_duplicate_document_state() { - let v=serde_json::to_string(&StudioDock::Libraries).unwrap(); - assert_eq!(serde_json::from_str::(&v).unwrap(),StudioDock::Libraries); - assert_eq!(StudioDock::ALL.len(),3); + let v = serde_json::to_string(&StudioDock::Libraries).unwrap(); + assert_eq!( + serde_json::from_str::(&v).unwrap(), + StudioDock::Libraries + ); + assert_eq!(StudioDock::ALL.len(), 3); } #[test] fn icon_geometry_does_not_change_source_in_egui_frame() { - let ctx=egui::Context::default(); - let mut source=String::from("% opaque CRLF\r\n\\newcommand{\\mine}{value}\r\n"); - let before=source.clone(); - let _=ctx.run(egui::RawInput::default(),|ctx|{ - egui::CentralPanel::default().show(ctx,|ui|{ - icon_button(ui,StudioIcon::Tool(Tool::Cubic),false,44.0,"Bezier"); - icon_button(ui,StudioIcon::Group(StudioGroup::Extensions),false,44.0,"Packages"); + let ctx = egui::Context::default(); + let mut source = String::from("% opaque CRLF\r\n\\newcommand{\\mine}{value}\r\n"); + let before = source.clone(); + let _ = ctx.run(egui::RawInput::default(), |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + icon_button(ui, StudioIcon::Tool(Tool::Cubic), false, 44.0, "Bezier"); + icon_button( + ui, + StudioIcon::Group(StudioGroup::Extensions), + false, + 44.0, + "Packages", + ); }); }); - assert_eq!(source,before); + assert_eq!(source, before); source.clear(); } } diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 30e5736d..957c4da7 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -248,34 +248,37 @@ fn show_guide_frame_mode( legacy_palette: bool, ) -> (Option, Rect) { if legacy_palette { - ui.horizontal_wrapped(|ui| { - ui.strong("TikZ · инструменты"); - for (index, tool) in TOOLS.into_iter().enumerate() { - let info = tikz_authoring::spec(tool); - let button = ui - .add(egui::Button::new(&info.title_ru).selected(session.active == Some(tool))) - .on_hover_text(format!( - "{}\n{}\nPGF/TikZ §{}\n{}", - info.help_ru, info.syntax, info.manual_section, info.manual_url - )); - if let Some(rects) = palette_rects.as_deref_mut() { - rects[index] = button.rect; - } - if button.clicked() { - session.select_tool(tool); + ui.horizontal_wrapped(|ui| { + ui.strong("TikZ · инструменты"); + for (index, tool) in TOOLS.into_iter().enumerate() { + let info = tikz_authoring::spec(tool); + let button = ui + .add(egui::Button::new(&info.title_ru).selected(session.active == Some(tool))) + .on_hover_text(format!( + "{}\n{}\nPGF/TikZ §{}\n{}", + info.help_ru, info.syntax, info.manual_section, info.manual_url + )); + if let Some(rects) = palette_rects.as_deref_mut() { + rects[index] = button.rect; + } + if button.clicked() { + session.select_tool(tool); + } } - } - }); - ui.strong(match session.active { - Some(tool) => format!("ВЫБРАН: {}", tikz_authoring::spec(tool).title_ru), - None => "ИНСТРУМЕНТ НЕ ВЫБРАН".to_owned(), - }); - ui.small("ПОЛЕ ВВОДА · шаг 0.25 cm. Esc отменяет клик/жест до отпускания мыши. Alt+клик на #ID выбирает команду. Это не PDF; стили и преобразования выполняет TeX."); + }); + ui.strong(match session.active { + Some(tool) => format!("ВЫБРАН: {}", tikz_authoring::spec(tool).title_ru), + None => "ИНСТРУМЕНТ НЕ ВЫБРАН".to_owned(), + }); + ui.small("ПОЛЕ ВВОДА · шаг 0.25 cm. Esc отменяет клик/жест до отпускания мыши. Alt+клик на #ID выбирает команду. Это не PDF; стили и преобразования выполняет TeX."); } else { ui.horizontal(|ui| { ui.strong("Эскиз TikZ"); if let Some(tool) = session.active { - ui.small(format!("Выбран: {} · шаг 0,25 cm", tikz_authoring::spec(tool).title_ru)); + ui.small(format!( + "Выбран: {} · шаг 0,25 cm", + tikz_authoring::spec(tool).title_ru + )); } else { ui.small("Выберите инструмент в палитре слева."); } diff --git a/crates/tikzarium-app/src/ui_theme.rs b/crates/tikzarium-app/src/ui_theme.rs index 038ea4cf..5d666b8b 100644 --- a/crates/tikzarium-app/src/ui_theme.rs +++ b/crates/tikzarium-app/src/ui_theme.rs @@ -1,8 +1,9 @@ //! Persistent egui appearance. This styling NEVER changes figure source or PDF colors. +use crate::studio_ui::StudioDock; use eframe::egui::{self, Color32, Stroke}; use serde::{Deserialize, Serialize}; use tikzarium_core::FigureFonts; -use crate::studio_ui::StudioDock; + pub const STORAGE_KEY: &str = "tikzarium.preferences.v1"; From 05006f8f2b29f35f24fdeb4a221c510435e420f8 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:19:15 +0300 Subject: [PATCH 08/52] style(B42): complete host rustfmt exact whitespace for icon Studio --- crates/tikzarium-app/src/main.rs | 1 - crates/tikzarium-app/src/studio_ui.rs | 1 - crates/tikzarium-app/src/ui_theme.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 496645ca..bde1d0da 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1821,7 +1821,6 @@ impl eframe::App for TikzariumApp { ui.label(&self.status); }); }); - } let split_layout = SplitPanelLayout::for_width(ctx.available_rect().width()); if self.prefs.studio_mode && self.prefs.studio_inspector_open { diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index f93e445b..767530dd 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -6,7 +6,6 @@ use eframe::egui::{self, Color32, Pos2, Rect, Sense, Shape, Stroke, Vec2}; use serde::{Deserialize, Serialize}; use tikzarium_extensions::primitives::Evidence; - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum StudioDock { #[default] diff --git a/crates/tikzarium-app/src/ui_theme.rs b/crates/tikzarium-app/src/ui_theme.rs index 5d666b8b..c29df416 100644 --- a/crates/tikzarium-app/src/ui_theme.rs +++ b/crates/tikzarium-app/src/ui_theme.rs @@ -4,7 +4,6 @@ use eframe::egui::{self, Color32, Stroke}; use serde::{Deserialize, Serialize}; use tikzarium_core::FigureFonts; - pub const STORAGE_KEY: &str = "tikzarium.preferences.v1"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] From d355c0cf1aead4690c86d8734c4f95a6c15a6b63 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:20:12 +0300 Subject: [PATCH 09/52] test(B42): real X11 Studio icon-palette screenshots with explicit bootstrap-only status --- .github/workflows/b42-studio-x11.yml | 47 ++++++++++++++ scripts/qa_b42_studio_x11.py | 92 ++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 .github/workflows/b42-studio-x11.yml create mode 100644 scripts/qa_b42_studio_x11.py diff --git a/.github/workflows/b42-studio-x11.yml b/.github/workflows/b42-studio-x11.yml new file mode 100644 index 00000000..ed11bdef --- /dev/null +++ b/.github/workflows/b42-studio-x11.yml @@ -0,0 +1,47 @@ +name: B42 real Studio icon palettes screenshot audit (bootstrap only) +on: + pull_request: + branches: [main] + paths: + - 'crates/tikzarium-app/**' + - 'scripts/qa_b42_studio_x11.py' + - '.github/workflows/b42-studio-x11.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + actual-studio: + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + CARGO_PROFILE_DEV_DEBUG: '0' + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec + with: + toolchain: '1.91.0' + - name: Install hosted X11 desktop prerequisites (not physical OS) + run: | + sudo apt-get update -q + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + xvfb xauth openbox xdotool imagemagick libgl1-mesa-dri libglx-mesa0 \ + libxkbcommon-dev libxkbcommon-x11-dev libx11-dev libxi-dev \ + libxcursor-dev libxrandr-dev libudev-dev + - name: Compile the actual Rust/egui executable from locked sources + run: | + cargo build --locked -p tikzarium-app --bin tikzarium-app + git diff --exit-code -- Cargo.lock + - name: Capture actual Studio and floating grouped palette from X11 + run: | + mkdir -p "$RUNNER_TEMP/b42-runtime" + chmod 700 "$RUNNER_TEMP/b42-runtime" + export XDG_RUNTIME_DIR="$RUNNER_TEMP/b42-runtime" + xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX +render -noreset' \ + bash -c 'openbox --sm-disable >/tmp/b42-openbox.log 2>&1 & python3 scripts/qa_b42_studio_x11.py --app "$GITHUB_WORKSPACE/target/debug/tikzarium-app" --out "$RUNNER_TEMP/b42-evidence"' + - name: Upload actual screenshots and explicit NOT_RUN ledger + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: b42-real-studio-${{ github.run_id }} + path: ${{ runner.temp }}/b42-evidence/ + if-no-files-found: warn diff --git a/scripts/qa_b42_studio_x11.py b/scripts/qa_b42_studio_x11.py new file mode 100644 index 00000000..4e77557b --- /dev/null +++ b/scripts/qa_b42_studio_x11.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""B42 bootstrap ONLY: inspect the real egui Studio window and floating palette. + +Screenshot success is NOT six-gesture, XeLaTeX, Win10, or physical GUI PASS. +""" +from __future__ import annotations +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +import time + +def cmd(*args: str, timeout: int = 25) -> str: + done = subprocess.run(args, capture_output=True, text=True, timeout=timeout, check=True) + return done.stdout.strip() + +def snapshot(destination: Path) -> None: + subprocess.run( + ["import", "-window", "root", str(destination)], + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30 + ) + if destination.stat().st_size < 5000: + raise RuntimeError(f"Screenshot unexpectedly empty: {destination}") + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--app", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + options = parser.parse_args() + options.out.mkdir(parents=True, exist_ok=True) + report = { + "schema_version": 1, + "kind": "B42_ACTUAL_EGUI_BOOTSTRAP", + "source_ui": "REAL_X11_PROCESS", + "six_native_gestures": "NOT_RUN", + "actual_xelatex_pdf": "NOT_RUN", + "physical_windows10": "NOT_RUN", + "physical_mac_m1_m4": "NOT_RUN", + "manual_screenshot_review": "NOT_RUN", + "status": "FAIL", + } + app_log = (options.out / "app.log").open("w", encoding="utf-8") + process = None + try: + env = os.environ.copy() + env["TIKZARIUM_XELATEX"] = "/nonexistent/offline-studio-bootstrap" + process = subprocess.Popen( + [str(options.app.resolve())], + env=env, stdout=app_log, stderr=subprocess.STDOUT, + ) + window = cmd("xdotool", "search", "--sync", "--onlyvisible", + "--name", "TikZarium", timeout=35).splitlines()[-1] + cmd("xdotool", "windowmove", window, "0", "0") + cmd("xdotool", "windowsize", "--sync", window, "1366", "768") + cmd("xdotool", "windowactivate", "--sync", window) + time.sleep(2.0) + snapshot(options.out / "01-studio-workspace.png") + # Candidate click only; its actual position and visual result require + # manual screenshot review before calling ANY palette selected. + cmd("xdotool", "mousemove", "--window", window, "30", "146") + cmd("xdotool", "click", "1") + time.sleep(0.8) + snapshot(options.out / "02-group-palette-candidate.png") + report["palette_selection"] = "CANDIDATE_COORDINATE_NOT_VERIFIED" + report["status"] = "BOOTSTRAP_CAPTURED_REQUIRES_MANUAL_VISUAL_REVIEW" + report["window_id"] = window + except Exception as error: + report["error"] = f"{type(error).__name__}: {error}" + finally: + if process is not None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + app_log.close() + report["files"] = { + item.name: hashlib.sha256(item.read_bytes()).hexdigest() + for item in options.out.iterdir() + if item.is_file() and item.name != "report.json" + } + (options.out / "report.json").write_text( + json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8" + ) + print(json.dumps(report, ensure_ascii=False)) + return 0 if report["status"].startswith("BOOTSTRAP_CAPTURED") else 1 + +if __name__ == "__main__": + raise SystemExit(main()) From 041578523d21ae55166fd4582f1530e30619d4b4 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:25:41 +0300 Subject: [PATCH 10/52] ux(B42): readable Midnight studio, registry-driven package palette and source-safe placement --- crates/tikzarium-app/src/main.rs | 22 ++++++++-- crates/tikzarium-app/src/studio_ui.rs | 53 +++++++++++++++++++------ crates/tikzarium-app/src/tikz_canvas.rs | 4 +- crates/tikzarium-app/src/ui_theme.rs | 11 ++++- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index bde1d0da..93ed7380 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -470,6 +470,7 @@ struct TikzariumApp { prefs: UiPreferences, studio_group: Option, studio_palette_anchor: egui::Pos2, + studio_extension_filter: String, authoring: tikz_authoring::Session, highlight_generated_line: bool, // A source patch or document replacement must not leave native TextEdit @@ -502,7 +503,7 @@ impl TikzariumApp { jump_to_line: None, last_build_failed: false, log: String::new(), - status: "Save the starter as a .tex file, then Build".into(), + status: "Сохраните новый рисунок как .tex, затем соберите PDF".into(), preview_status: "No exact preview yet".into(), preview_texture: None, preview_lod_texture: None, @@ -532,6 +533,7 @@ impl TikzariumApp { prefs, studio_group: None, studio_palette_anchor: egui::pos2(65.0, 120.0), + studio_extension_filter: String::new(), authoring: { let mut state = tikz_authoring::Session::default(); state.reset(QUICKSTART); @@ -981,7 +983,7 @@ impl TikzariumApp { if self.pending_open.is_some() { ui.disable(); } - ui.heading("TeX / TikZ source"); + ui.heading("Исходный TikZ / TeX"); ui.small( self.document .path @@ -1230,10 +1232,17 @@ impl TikzariumApp { } }); ui.horizontal(|ui| { + let prior_focus = self.pdf_focus_in_split; ui.selectable_value(&mut self.pdf_focus_in_split, false, "Эскиз"); ui.selectable_value(&mut self.pdf_focus_in_split, true, "PDF"); - if self.pdf_focus_in_split { + if self.pdf_focus_in_split && !prior_focus { self.fit_page_active = true; + self.authoring.cancelled_pointer_capture |= + self.authoring.guide_pointer_armed + || self.authoring.drag_start.is_some(); + self.authoring.guide_pointer_armed = false; + self.authoring.drag_start = None; + self.authoring.alt_selection_capture = false; } ui.separator(); let filename = self @@ -1982,9 +1991,14 @@ impl eframe::App for TikzariumApp { studio_ui::show_palette( ctx, &mut self.studio_group, - self.studio_palette_anchor, + egui::pos2( + self.studio_palette_anchor.x + split_layout.source_default + 18.0, + self.studio_palette_anchor.y, + ), &mut self.authoring, &self.document.source, + &self.package_statuses, + &mut self.studio_extension_filter, &mut open_libraries, ); if open_libraries { diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 767530dd..697b537a 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -2,9 +2,8 @@ //! Tool and parameter semantics belong to the existing C catalogue. Vector //! icon strokes are only UI affordances, never a second PGF renderer. use crate::tikz_authoring::{self, Session, Tool}; -use eframe::egui::{self, Color32, Pos2, Rect, Sense, Shape, Stroke, Vec2}; +use eframe::egui::{self, Pos2, Rect, Sense, Shape, Stroke, Vec2}; use serde::{Deserialize, Serialize}; -use tikzarium_extensions::primitives::Evidence; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum StudioDock { @@ -330,6 +329,8 @@ pub fn show_palette( anchor: Pos2, session: &mut Session, source: &str, + extensions: &[tikzarium_extensions::ExtensionStatus], + extension_filter: &mut String, open_libraries: &mut bool, ) { if ctx.input(|input| input.key_pressed(egui::Key::Escape)) { @@ -341,7 +342,11 @@ pub fn show_palette( .fixed_pos(anchor+egui::vec2(6.0,-36.0)) .show(ctx, |ui| { egui::Frame::popup(ui.style()).show(ui, |ui| { - ui.set_min_width(158.0); + ui.set_min_width(if selected == StudioGroup::Extensions { + 265.0 + } else { + 158.0 + }); ui.horizontal(|ui|{ ui.strong(selected.label()); if ui.small_button("×").on_hover_text("Закрыть палитру").clicked(){ @@ -349,15 +354,39 @@ pub fn show_palette( } }); ui.separator(); - if selected==StudioGroup::Extensions { - ui.small("Пакеты / библиотеки"); - for name in ["PGFPlots","CircuitikZ","tikz-cd"] { - ui.add_enabled(false,egui::Button::new(name)) - .on_disabled_hover_text("Графический адаптер ещё не реализован. Обычный исходный TeX остаётся доступен."); - } - if ui.button("Показать все пакеты").clicked(){ - *open_libraries=true; - *group=None; + if selected == StudioGroup::Extensions { + ui.small("Расширения из общего реестра"); + ui.add( + egui::TextEdit::singleline(extension_filter) + .hint_text("Поиск пакета…") + .desired_width(242.0), + ); + let filter = extension_filter.trim().to_lowercase(); + egui::ScrollArea::vertical() + .max_height(270.0) + .show(ui, |ui| { + for item in extensions.iter().filter(|item| { + filter.is_empty() + || item.manifest.id.to_lowercase().contains(&filter) + || item.manifest.display_name.to_lowercase().contains(&filter) + }) { + ui.strong(&item.manifest.display_name); + ui.small(format!( + "Установлен: {:?} · Совместимость: {:?}", + item.installation, item.compilation + )); + ui.small(format!( + "GUI-инструмент: {} · просмотр: {}", + if item.visual_editable { "есть" } else { "пока нет" }, + if item.inspectable { "есть" } else { "пока нет" }, + )); + ui.separator(); + } + }); + ui.small("Наличие ≠ подключение ≠ поддержка инструмента. Lua-only не XeLaTeX."); + if ui.button("Управление библиотеками проекта").clicked() { + *open_libraries = true; + *group = None; } } else { ui.horizontal_wrapped(|ui|{ diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 957c4da7..384cf44d 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -291,7 +291,9 @@ fn show_guide_frame_mode( && project_state == Some(tikzarium_extensions::primitives::Evidence::Confirmed); // Missing XeLaTeX/tikz.sty is a build-time warning, not a veto on safe // source patches. Keep import UNKNOWN/ABSENT fail-closed independently. - if !ready || !caps.exact_build_prerequisites_verified() { + if (legacy_palette || session.active.is_some()) + && (!ready || !caps.exact_build_prerequisites_verified()) + { ui.colored_label( ui.visuals().warn_fg_color, if !caps.source_authoring_supported() || ready { diff --git a/crates/tikzarium-app/src/ui_theme.rs b/crates/tikzarium-app/src/ui_theme.rs index c29df416..0ed5301f 100644 --- a/crates/tikzarium-app/src/ui_theme.rs +++ b/crates/tikzarium-app/src/ui_theme.rs @@ -8,9 +8,9 @@ pub const STORAGE_KEY: &str = "tikzarium.preferences.v1"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UiTheme { - #[default] Light, Dark, + #[default] Midnight, Paper, } @@ -65,6 +65,15 @@ impl UiTheme { egui::vec2(8.0, 6.0) }; style.spacing.interact_size.y = if compact { 23.0 } else { 29.0 }; + style + .text_styles + .insert(egui::TextStyle::Body, egui::FontId::proportional(14.0)); + style + .text_styles + .insert(egui::TextStyle::Monospace, egui::FontId::monospace(14.0)); + style + .text_styles + .insert(egui::TextStyle::Small, egui::FontId::proportional(12.0)); }); } } From c43d5f8e4798dab758c68723f739a9bac8718d87 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:26:18 +0300 Subject: [PATCH 11/52] docs(B42): record reviewed real X11 UX defects and corrected Studio acceptance --- CHANGELOG.md | 2 ++ insights/2026-11-02_B42_UX_EVIDENCE.md | 8 ++++++++ plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 10 ++++++++++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 526f0a20..76a21636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased — B42 redesigned Studio and dedicated user's MiKTeX Windows Server QA (PENDING) +- Follow-up from real B42 X11 screenshot review: restore a dark Midnight default and readable 14pt source/body fonts, relocate grouped palette away from source, search the live C registry instead of hard-coded packages, and keep manual PDF zoom stable when selecting PDF. Exact-head reacceptance PENDING. + - Replaced repeated long tool buttons with native egui vector icons in four grouped floating palettes. No font-file asset or second PGF renderer; native C-catalogue commands and source-first semantics retained. - Added a concise icon toolbar, permanently editable TeX beside the center drawing/PDF, independent Properties/Libraries/Help tabs, persistent workspace preferences and collapsed-by-default diagnostics. Classic UI remains temporarily available for regression comparisons. - Added a manual main-SHA-gated GitHub Actions test for a dedicated user Windows Server runner and a no-auto-install MiKTeX/locked-Rust proof script. Runner connectivity and physical Windows10/M1/M4 are NOT_RUN; this is not GUI Session proof. diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index e9996bbc..ea136402 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -20,3 +20,11 @@ persistent Windows Server test NOT_RUN; physical Win10/M1/M4 NOT_RUN. Risk: old coordinate-pinned desktop drivers must be recalibrated for new layout, not marked PASS from historical B41. Keep existing CI gates. + +## First real B42 Linux/X11 screenshots reviewed + +Actual executable/bootstrap [run 41412320640](https://github.com/i-shutov/tikzarium/actions/runs/41412320640), artifact 13126535781, was downloaded and the two real PNGs independently inspected. Group Geometry popup **really opens** with four vector icons; no generated TikZ or XeLaTeX claim. Found remaining UX problems: Light initial palette too pale, small text, popup overlapping the editable source, and an unnecessary orange warning before choosing a tool. + +This B42 correction changes default to Midnight, increases Body/Monospace font sizes, moves popup over the center workspace, omits empty-tool warning, and presents package entries from the actual C extension Registry with independent installed/engine/tool states rather than hard-coded three-name buttons. Focusing the PDF no longer resets manual zoom on every subsequent frame. Requires new exact-head Rust and GUI screenshots before acceptance. The old preview screenshot status stays historical, not a new PASS. + +B42 first Rust/D QA compiled and passed unit tests on Ubuntu, but strict Clippy found unused imports. These were removed in the current B42 head. All updated CI + actual final GUI **NOT_RUN** until complete. The Windows Server runner and physical Win10/M1/M4 remain NOT_RUN. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index e2a257e7..11106bb1 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -18,3 +18,13 @@ no hidden installer, actual PDF inventory/hash, cargo --locked; self-hosted runner is not known connected yet. Windows Server != Windows10. B42 exact-head hosted CI + real X11 screenshots and user-server external test are release gates, never infer them from source changes alone. + +## UX revision after actual X11 bootstrap + +- Default theme Midnight, retained 4 persistent UI themes. +- Monospace text 14pt, body 14pt, small 12pt for readable code/help. +- Popover over canvas rather than over source (actual screenshot showed overlap). +- Extension popup is data-driven from C Registry (no fixed top-three list); search and 270pt scroll cap for growth. +- Eliminate duplicate first-load orange error with no selected tool. +- PDF focus changes only on selection transition; preserve manual zoom. +- Re-run real X11 screenshot and all original source/Undo/TeX checks on revised exact HEAD before merging. From c07df47199a7348f16390f04069c72af01dd3dd5 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:28:04 +0300 Subject: [PATCH 12/52] style(B42): rustfmt normalized registry-driven palettes and PDF focus transition --- crates/tikzarium-app/src/main.rs | 3 +- crates/tikzarium-app/src/studio_ui.rs | 55 +++++++++++++++++++-------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 93ed7380..e94462bd 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1238,8 +1238,7 @@ impl TikzariumApp { if self.pdf_focus_in_split && !prior_focus { self.fit_page_active = true; self.authoring.cancelled_pointer_capture |= - self.authoring.guide_pointer_armed - || self.authoring.drag_start.is_some(); + self.authoring.guide_pointer_armed || self.authoring.drag_start.is_some(); self.authoring.guide_pointer_armed = false; self.authoring.drag_start = None; self.authoring.alt_selection_capture = false; diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 697b537a..e0ad2fab 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -339,7 +339,7 @@ pub fn show_palette( let Some(selected) = *group else { return }; egui::Area::new(egui::Id::new("b42-group-palette")) .order(egui::Order::Foreground) - .fixed_pos(anchor+egui::vec2(6.0,-36.0)) + .fixed_pos(anchor + egui::vec2(6.0, -36.0)) .show(ctx, |ui| { egui::Frame::popup(ui.style()).show(ui, |ui| { ui.set_min_width(if selected == StudioGroup::Extensions { @@ -347,10 +347,14 @@ pub fn show_palette( } else { 158.0 }); - ui.horizontal(|ui|{ + ui.horizontal(|ui| { ui.strong(selected.label()); - if ui.small_button("×").on_hover_text("Закрыть палитру").clicked(){ - *group=None; + if ui + .small_button("×") + .on_hover_text("Закрыть палитру") + .clicked() + { + *group = None; } }); ui.separator(); @@ -377,36 +381,55 @@ pub fn show_palette( )); ui.small(format!( "GUI-инструмент: {} · просмотр: {}", - if item.visual_editable { "есть" } else { "пока нет" }, - if item.inspectable { "есть" } else { "пока нет" }, + if item.visual_editable { + "есть" + } else { + "пока нет" + }, + if item.inspectable { + "есть" + } else { + "пока нет" + }, )); ui.separator(); } }); ui.small("Наличие ≠ подключение ≠ поддержка инструмента. Lua-only не XeLaTeX."); - if ui.button("Управление библиотеками проекта").clicked() { + if ui.button("Управление библиотеками проекта").clicked() + { *open_libraries = true; *group = None; } } else { - ui.horizontal_wrapped(|ui|{ + ui.horizontal_wrapped(|ui| { for &tool in selected.tools() { - let info=tikz_authoring::spec(tool); - let status=tikz_authoring::requirements_status(source,tool); - let tip=format!( + let info = tikz_authoring::spec(tool); + let status = tikz_authoring::requirements_status(source, tool); + let tip = format!( "{}\n{}\n{}\nPGF/TikZ §{}\nПодключение: {:?}", - info.title_ru,info.help_ru,info.syntax,info.manual_section,status + info.title_ru, + info.help_ru, + info.syntax, + info.manual_section, + status ); if icon_button( - ui,StudioIcon::Tool(tool),session.active==Some(tool),46.0,&tip - ).clicked() { + ui, + StudioIcon::Tool(tool), + session.active == Some(tool), + 46.0, + &tip, + ) + .clicked() + { session.select_tool(tool); - *group=None; + *group = None; } } }); ui.small("Наведите на иконку для команды и справки."); - if selected==StudioGroup::Connections { + if selected == StudioGroup::Connections { ui.small("Стрелки arrows.meta требуют явного подключения."); } } From bf4b1ef9ee5bed4ca1fad61e18a378e3a5faca3d Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:29:05 +0300 Subject: [PATCH 13/52] ux(B42): full-size TikZ drawing workspace or full-size PDF, code always beside both --- crates/tikzarium-app/src/main.rs | 26 +++++++++++++++++++++++++ crates/tikzarium-app/src/tikz_canvas.rs | 7 ++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index e94462bd..6b92fe13 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1348,6 +1348,32 @@ impl TikzariumApp { } fn show_pdf_canvas(&mut self, ui: &mut egui::Ui) { + // Studio uses the full central area for ONE primary task: interactive + // TikZ authoring OR exact PDF. The previous 170pt pad above a huge + // empty preview was the principal ergonomic failure on 1366x768. + // Both modes keep the original editable code pane alongside them. + if self.prefs.studio_mode && !self.pdf_focus_in_split { + let prior = self.authoring.source_revision(); + let caps = self.primitive_capabilities(); + if let Some(line) = tikz_canvas::show_guide_studio( + ui, + &mut self.authoring, + &mut self.document.source, + caps, + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; + ui.ctx().request_repaint(); + } + fence_source_editor_undo_on_revision_change( + prior, + &self.authoring, + &mut self.reset_source_editor_undo_history, + ); + ui.separator(); + ui.small("Интерактивный эскиз TikZ. Точная геометрия и шрифты — во вкладке PDF после XeLaTeX."); + return; + } let canvas_width = ui.available_width(); let mut displayed_zoom_percent = pdf_zoom_percent(self.zoom); ui.horizontal_wrapped(|ui| { diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 384cf44d..2abdb7a3 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -309,8 +309,13 @@ fn show_guide_frame_mode( }, ); } + let height = if legacy_palette { + HEIGHT + } else { + (ui.available_height() - 36.0).clamp(200.0, 700.0) + }; let (rect, response) = ui.allocate_exact_size( - Vec2::new(ui.available_width().max(180.0), HEIGHT), + Vec2::new(ui.available_width().max(180.0), height), Sense::click_and_drag(), ); let painter = ui.painter_at(rect); From 0f51a33615fae1f358ad46a20b55ba7e1cc051fc Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:30:55 +0300 Subject: [PATCH 14/52] fix(B42): group C-registry palette inputs into typed context for strict Clippy --- crates/tikzarium-app/src/main.rs | 8 +++++--- crates/tikzarium-app/src/studio_ui.rs | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 6b92fe13..fc05b7e3 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -2021,9 +2021,11 @@ impl eframe::App for TikzariumApp { self.studio_palette_anchor.y, ), &mut self.authoring, - &self.document.source, - &self.package_statuses, - &mut self.studio_extension_filter, + studio_ui::PaletteData { + source: &self.document.source, + extensions: &self.package_statuses, + filter: &mut self.studio_extension_filter, + }, &mut open_libraries, ); if open_libraries { diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index e0ad2fab..815191eb 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -323,16 +323,27 @@ pub fn show_group_rail( /// Floating grouped palette. It never takes space from the code/PDF columns. /// Future package adapters appear as unavailable, not hidden or falsely ready. +/// Borrow the current read-only project facts as one UI context, not a +/// second registry and not a license to mutate user source or install TeX. +pub struct PaletteData<'a> { + pub source: &'a str, + pub extensions: &'a [tikzarium_extensions::ExtensionStatus], + pub filter: &'a mut String, +} + pub fn show_palette( ctx: &egui::Context, group: &mut Option, anchor: Pos2, session: &mut Session, - source: &str, - extensions: &[tikzarium_extensions::ExtensionStatus], - extension_filter: &mut String, + data: PaletteData<'_>, open_libraries: &mut bool, ) { + let PaletteData { + source, + extensions, + filter: extension_filter, + } = data; if ctx.input(|input| input.key_pressed(egui::Key::Escape)) { *group = None; } From fdf4e076cbeccf2464c68242042a5ca43d21f3dc Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:32:55 +0300 Subject: [PATCH 15/52] ux(B42): remove inspector clutter until a native TikZ tool is selected --- crates/tikzarium-app/src/main.rs | 68 ++++++++++++++----------- crates/tikzarium-app/src/tikz_canvas.rs | 16 +++--- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index fc05b7e3..eee73f87 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1269,41 +1269,51 @@ impl TikzariumApp { } }); ui.separator(); - let prior = self.authoring.source_revision(); - if let Some(line) = tikz_canvas::show_inspector_action_bar( - ui, - &mut self.authoring, - &mut self.document.source, - ) { - self.jump_to_line = Some(line); - self.highlight_generated_line = true; + if self.authoring.active.is_some() || self.authoring.undo_available() { + let prior = self.authoring.source_revision(); + if let Some(line) = tikz_canvas::show_inspector_action_bar( + ui, + &mut self.authoring, + &mut self.document.source, + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; + } + fence_source_editor_undo_on_revision_change( + prior, + &self.authoring, + &mut self.reset_source_editor_undo_history, + ); + ui.separator(); } - fence_source_editor_undo_on_revision_change( - prior, - &self.authoring, - &mut self.reset_source_editor_undo_history, - ); - ui.separator(); egui::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| match self.prefs.studio_dock { StudioDock::Properties => { - let prior = self.authoring.source_revision(); - let caps = self.primitive_capabilities(); - if let Some(line) = tikz_canvas::show_inspector( - ui, - &mut self.authoring, - &mut self.document.source, - caps, - ) { - self.jump_to_line = Some(line); - self.highlight_generated_line = true; + if self.authoring.active.is_none() { + ui.heading("Выберите инструмент"); + ui.label("Нажмите на группу иконок слева, затем выберите команду TikZ."); + ui.small("Параметры и примеры появятся здесь из единого каталога."); + ui.separator(); + ui.small("Исходный .tex остаётся редактируемым. Точный результат — внешний XeLaTeX."); + } else { + let prior = self.authoring.source_revision(); + let caps = self.primitive_capabilities(); + if let Some(line) = tikz_canvas::show_inspector( + ui, + &mut self.authoring, + &mut self.document.source, + caps, + ) { + self.jump_to_line = Some(line); + self.highlight_generated_line = true; + } + fence_source_editor_undo_on_revision_change( + prior, + &self.authoring, + &mut self.reset_source_editor_undo_history, + ); } - fence_source_editor_undo_on_revision_change( - prior, - &self.authoring, - &mut self.reset_source_editor_undo_history, - ); } StudioDock::Libraries => { ui.heading("Библиотеки проекта"); diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 2abdb7a3..28fd9f51 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -871,13 +871,15 @@ pub fn show_inspector( let mut jump = None; ui.heading("TikZ · параметры и справка"); ui.small("Один каталог: палитра → свойства → команда → F1. Неизвестный TeX не переписывается."); - ui.small(format!( - "Установлен tikz.sty={} · явно подключён={} · внешний XeLaTeX={} · B-инструмент=реализован · C-справка=доступна", - if caps.installed { "CONFIRMED" } else { "UNKNOWN" }, - evidence_label(caps.loaded), - if caps.engine_compatible { "CONFIRMED" } else { "UNKNOWN" }, - )); - ui.small(caps.explanation()); + ui.collapsing("Техническая готовность TikZ / XeLaTeX", |ui| { + ui.small(format!( + "Установлен tikz.sty={} · явно подключён={} · внешний XeLaTeX={} · GUI-инструмент=реализован · справка=доступна", + if caps.installed { "CONFIRMED" } else { "UNKNOWN" }, + evidence_label(caps.loaded), + if caps.engine_compatible { "CONFIRMED" } else { "UNKNOWN" }, + )); + ui.small(caps.explanation()); + }); let choices = tikz_authoring::owned(source); egui::ComboBox::from_id_salt("n1-owned-command") .selected_text( From ea1d8cf14264ba1e83eb31ff37230d6ba14eeadd Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:38:09 +0300 Subject: [PATCH 16/52] test(B42): exercise all six Studio guide pointer handlers with real egui RawInput --- crates/tikzarium-app/src/tikz_canvas.rs | 103 ++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 28fd9f51..704c026b 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -2125,6 +2125,109 @@ mod tests { } } + // UNIT_EVENT: exercise the Studio guide, not the legacy tool-row guide. + // Group-palette selection is tested separately: Studio deliberately shares + // this production pointer handler and the C-owned source patch path. + fn b42_studio_event_frame( + ctx: &egui::Context, + session: &mut Session, + source: &mut String, + events: Vec, + viewport_width: f32, + ) -> (Option, Rect) { + let mut observed = None; + let mut guide_rect = Rect::NOTHING; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(Rect::from_min_size( + Pos2::ZERO, + Vec2::new(viewport_width, 640.0), + )), + events, + ..Default::default() + }, + |ctx| { + handle_help_shortcut(ctx, session); + egui::CentralPanel::default().show(ctx, |ui| { + (observed, guide_rect) = + show_guide_frame_mode(ui, session, source, READY, None, false); + }); + }, + ); + (observed, guide_rect) + } + + #[test] + fn b42_studio_six_pointer_gestures_reopen_and_undo_use_actual_egui_frames() { + for width in [850.0, 1366.0] { + for tool in TOOLS { + let ctx = egui::Context::default(); + let mut source = EVENT_SOURCE.to_owned(); + let before = source.clone(); + let mut session = Session::default(); + session.reset(&source); + session.select_tool(tool); + assert_eq!(source, before, "{tool:?}: selection changed source"); + let (_, guide) = + b42_studio_event_frame(&ctx, &mut session, &mut source, vec![], width); + assert!(guide.width() > 190.0 && guide.height() > 190.0); + let start = screen_point(guide, Point::new(1.0, 1.0)); + let end = screen_point(guide, Point::new(3.0, 2.0)); + b42_studio_event_frame( + &ctx, + &mut session, + &mut source, + vec![egui::Event::PointerMoved(start), primary(start, true)], + width, + ); + b42_studio_event_frame( + &ctx, + &mut session, + &mut source, + vec![egui::Event::PointerMoved(end)], + width, + ); + let (line, _) = + b42_studio_event_frame(&ctx, &mut session, &mut source, vec![primary(end, false)], width); + let line = line.expect("Studio pointer release must focus authored command"); + let owned = tikz_authoring::owned(&source); + assert_eq!(owned.len(), 1, "{tool:?}: missing or duplicate C command"); + assert_eq!(owned[0].tool, tool); + assert_eq!(owned[0].line, line); + assert!(source.contains(&tikz_authoring::command(tool, &owned[0].draft).unwrap())); + assert!(source.contains(r"\myOpaqueMacro{a_b}")); + assert!(source.contains("% пользовательский непрозрачный материал")); + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("studio.tex"); + std::fs::write(&file, &source).unwrap(); + let saved = std::fs::read_to_string(&file).unwrap(); + assert_eq!(saved, source); + let mut reopened = Session::default(); + reopened.reset(&saved); + assert_eq!(reopened.choose(&saved, 1).unwrap(), line); + let after_drawing = source.clone(); + let help_before = session.help; + b42_studio_event_frame( + &ctx, + &mut session, + &mut source, + vec![egui::Event::Key { + key: egui::Key::F1, + physical_key: None, + pressed: true, + repeat: false, + modifiers: egui::Modifiers::default(), + }], + width, + ); + assert_ne!(session.help, help_before, "{tool:?}: Studio F1 failed"); + assert_eq!(source, after_drawing, "{tool:?}: F1 touched source"); + session.undo(&mut source).unwrap(); + assert_eq!(source, before, "{tool:?}: Studio Undo changed user TeX"); + } + } + } + #[test] fn actual_egui_pointer_frames_create_six_c_catalogue_commands_and_preserve_source() { for tool in TOOLS { From e63140674d3360872f04323c8bed966a7dc8f69d Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:38:38 +0300 Subject: [PATCH 17/52] docs(B42): record Studio real-handler event gate and evidence limits --- plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index 11106bb1..fa88c727 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -28,3 +28,18 @@ test are release gates, never infer them from source changes alone. - Eliminate duplicate first-load orange error with no selected tool. - PDF focus changes only on selection transition; preserve manual zoom. - Re-run real X11 screenshot and all original source/Undo/TeX checks on revised exact HEAD before merging. + +## B42 UNIT_EVENT Studio gesture regression (additional B-owned coverage) + +A direct `egui::Context::run(RawInput)` test now drives the **Studio** guide +(`show_guide_frame_mode(..., legacy_palette=false)`) at 850 and 1366 pt. +For each of the six C-owned primitives it sends actual pointer down/move/up, +asserts exactly one authored C command and the selected source line, then +checks on-disk Save/Reopen, F1 without a source mutation, and byte-exact visual +Undo preserving an opaque macro and Russian comment. + +This protects against the B42 UI refactor inadvertently bypassing the +production pointer handler. Palette group icon selection, the full desktop +layout, source native Ctrl+Z and external XeLaTeX still require independent +real X11 evidence. The test is **UNIT_EVENT**, not Linux/X11 process QA and +not physical Windows 10 or macOS ARM acceptance. From 89eda38d3ff5341faf6c4f3b11c0e13f1dca4b58 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:38:41 +0300 Subject: [PATCH 18/52] docs(B42): record Studio real-handler event gate and evidence limits --- insights/2026-11-02_B42_UX_EVIDENCE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index ea136402..a9fb7e2c 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -28,3 +28,19 @@ Actual executable/bootstrap [run 41412320640](https://github.com/i-shutov/tikzar This B42 correction changes default to Midnight, increases Body/Monospace font sizes, moves popup over the center workspace, omits empty-tool warning, and presents package entries from the actual C extension Registry with independent installed/engine/tool states rather than hard-coded three-name buttons. Focusing the PDF no longer resets manual zoom on every subsequent frame. Requires new exact-head Rust and GUI screenshots before acceptance. The old preview screenshot status stays historical, not a new PASS. B42 first Rust/D QA compiled and passed unit tests on Ubuntu, but strict Clippy found unused imports. These were removed in the current B42 head. All updated CI + actual final GUI **NOT_RUN** until complete. The Windows Server runner and physical Win10/M1/M4 remain NOT_RUN. + +## Studio-mode six-primitive event-frame evidence (B42 QA extension) + +An audit found that existing complete six-gesture `RawInput` tests called +the **legacy** guide, while B42 added a Studio branch removing its tool row. +Added `b42_studio_six_pointer_gestures_reopen_and_undo_use_actual_egui_frames` +in the same B42 PR: 2 viewport widths × 6 native C tools, actual egui pointer +press/move/release, focus line, C-produced command, file Save/Reopen, +Russian/opaque source preservation, F1 and byte-exact visual Undo. No +catalogue regeneration and no new solver. Existing legacy tests remain intact. + +Authoring checkpoint: branch commit `ea1d8cf14264ba1e83eb31ff37230d6ba14eeadd`. +**No claim of test PASS until exact-head hosted CI completes**; no local +Rust toolchain or physical Win10/M1/M4 runtime available in this environment. +A real Studio grouped-palette click and real X11 source Ctrl+Z/Save/Open +with independent screenshots remain a separate gate. From 2909e4684c8743462ccf205adf51c1ee94ac0c8b Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:40:37 +0300 Subject: [PATCH 19/52] fix(B42): anchor tool palette to resized source pane and clear hidden rail popup --- crates/tikzarium-app/src/main.rs | 36 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index eee73f87..d24c1553 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -79,13 +79,15 @@ fn show_split_source_panel( ctx: &egui::Context, layout: SplitPanelLayout, add_contents: impl FnOnce(&mut egui::Ui), -) { +) -> egui::Rect { egui::SidePanel::left("source") .resizable(true) .default_width(layout.source_default) .min_width(240.0) .max_width(layout.source_max) - .show(ctx, add_contents); + .show(ctx, add_contents) + .response + .rect } /// Fit an entire rasterized XeLaTeX PDF page in the actual remaining preview @@ -1192,7 +1194,14 @@ impl TikzariumApp { } }); ui.menu_button("Вид", |ui| { - ui.checkbox(&mut self.prefs.studio_tool_rail, "Группы инструментов"); + if ui + .checkbox(&mut self.prefs.studio_tool_rail, "Группы инструментов") + .changed() + && !self.prefs.studio_tool_rail + { + // A hidden rail must not leave a floating tool palette. + self.studio_group = None; + } ui.checkbox(&mut self.prefs.studio_inspector_open, "Инспектор"); ui.checkbox(&mut self.prefs.studio_console_open, "Диагностика"); ui.separator(); @@ -2006,11 +2015,13 @@ impl eframe::App for TikzariumApp { } }); } - if self.workspace == WorkspaceMode::Split { - show_split_source_panel(ctx, split_layout, |ui| { + let actual_source_rect = if self.workspace == WorkspaceMode::Split { + Some(show_split_source_panel(ctx, split_layout, |ui| { self.show_source_editor(ui); - }); - } + })) + } else { + None + }; egui::CentralPanel::default().show(ctx, |ui| { if self.pending_open.is_some() { ui.disable(); @@ -2021,15 +2032,16 @@ impl eframe::App for TikzariumApp { self.show_source_editor(ui); } }); - if self.prefs.studio_mode && self.pending_open.is_none() { + if self.prefs.studio_mode + && self.prefs.studio_tool_rail + && self.pending_open.is_none() + && let Some(source_rect) = actual_source_rect + { let mut open_libraries = false; studio_ui::show_palette( ctx, &mut self.studio_group, - egui::pos2( - self.studio_palette_anchor.x + split_layout.source_default + 18.0, - self.studio_palette_anchor.y, - ), + studio_ui::palette_anchor_at_source_edge(source_rect, self.studio_palette_anchor), &mut self.authoring, studio_ui::PaletteData { source: &self.document.source, From 312ffd86d648ae0a99937b18735cf7d8de064304 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:40:54 +0300 Subject: [PATCH 20/52] test(B42): assert palette tracks actual resized source-pane boundary --- crates/tikzarium-app/src/studio_ui.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 815191eb..3c3b3699 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -321,6 +321,13 @@ pub fn show_group_rail( anchor } +/// Position the popup from the ACTUAL resizable source SidePanel boundary, +/// never from its default width. No PDF-to-guide coordinate mapping is implied. +/// The floating palette itself adds a small 6pt gap in show_palette. +pub fn palette_anchor_at_source_edge(source_panel: Rect, rail_anchor: Pos2) -> Pos2 { + Pos2::new(source_panel.right() + 8.0, rail_anchor.y) +} + /// Floating grouped palette. It never takes space from the code/PDF columns. /// Future package adapters appear as unavailable, not hidden or falsely ready. /// Borrow the current read-only project facts as one UI context, not a @@ -539,6 +546,21 @@ mod tests { ); assert_eq!(StudioDock::ALL.len(), 3); } + #[test] + fn palette_follows_actual_source_pane_resize_not_default_width() { + // Simulated measured response rectangles from a resizable source panel. + let button = Pos2::new(61.0, 178.0); + let narrow = Rect::from_min_size(Pos2::new(62.0, 60.0), Vec2::new(240.0, 600.0)); + let wide = Rect::from_min_size(Pos2::new(62.0, 60.0), Vec2::new(475.0, 600.0)); + let a = palette_anchor_at_source_edge(narrow, button); + let b = palette_anchor_at_source_edge(wide, button); + assert_eq!(a.x, narrow.right() + 8.0); + assert_eq!(b.x, wide.right() + 8.0); + assert_eq!(b.x - a.x, 235.0); + assert_eq!(a.y, button.y); + assert_eq!(b.y, button.y); + } + #[test] fn icon_geometry_does_not_change_source_in_egui_frame() { let ctx = egui::Context::default(); From 5b8afcd5bb38d7a494f85d6767755fbfd3ec8a9d Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:41:12 +0300 Subject: [PATCH 21/52] docs(B42): record resized panel UX fix, real event coverage and X11 limits --- ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index fa88c727..6d36b1e7 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -43,3 +43,22 @@ production pointer handler. Palette group icon selection, the full desktop layout, source native Ctrl+Z and external XeLaTeX still require independent real X11 evidence. The test is **UNIT_EVENT**, not Linux/X11 process QA and not physical Windows 10 or macOS ARM acceptance. + +## B42 resized-source palette placement and hidden-rail consistency + +Reproducible code-path UX defect: `main.rs` positioned the floating +tool palette using `SplitPanelLayout::source_default`, while the editable +source SidePanel is `resizable(true)`. After the user changed its width, +the palette no longer started at the actual source/guide boundary and could +obscure the code or float unexpectedly. The View menu could also hide the +tool rail without closing an already open floating palette. + +The production fix now reads the real `SidePanel::show(...).response.rect`, +anchors the palette at `source_rect.right()+8pt`, gates the popup on the +visible rail and closes its open group when the rail is disabled. A separate +test covers both wide and narrow actual-boundary input rectangles; the +B42 real-RawInput six-gesture test covers code patches independently. + +Acceptance still requires a real X11 resize/rail hide/reopen screenshot +sequence plus six live Studio gestures, genuine source Ctrl+Z and PDF. +Synthetic rectangles and `UNIT_EVENT` do not prove real resizable GUI. From 8e264592527260c3fc955d2ad61d2b279b9ba508 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:41:15 +0300 Subject: [PATCH 22/52] docs(B42): record resized panel UX fix, real event coverage and X11 limits --- insights/2026-11-02_B42_UX_EVIDENCE.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index a9fb7e2c..489b9826 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -44,3 +44,20 @@ Authoring checkpoint: branch commit `ea1d8cf14264ba1e83eb31ff37230d6ba14eeadd`. Rust toolchain or physical Win10/M1/M4 runtime available in this environment. A real Studio grouped-palette click and real X11 source Ctrl+Z/Save/Open with independent screenshots remain a separate gate. + +## Resizable source/palette defect and correction + +Static code audit of B42 identified a reproducible mismatch: +`show_split_source_panel` has `resizable(true)`, yet the palette X +position was `studio_palette_anchor.x + split_layout.source_default + 18`. +The default width is not the current source panel width after a resize. +Likewise, turning off the tool rail did not prevent `show_palette` from +continuing to render the old selected group. + +Changed only B-owned `main.rs`/`studio_ui.rs`: use the measured egui +source SidePanel response rectangle, hide/close the palette with the rail, +and regression-check measured-boundary arithmetic for both 240pt/475pt +source widths. App code commit `2909e4684c8743462ccf205adf51c1ee94ac0c8b`; +helper/test commit `312ffd86d648ae0a99937b18735cf7d8de064304`. +Full GUI manual resize verification: **NOT_RUN** until exact-head X11 +artifact and screenshots are reviewed. Physical Win10/M1/M4: **NOT_RUN**. From 1245894f963ecb15170b5fa5e3325d7c59039577 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:41:45 +0300 Subject: [PATCH 23/52] style(B42): apply exact hosted rustfmt diagnostic to Studio gesture test --- crates/tikzarium-app/src/tikz_canvas.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/tikzarium-app/src/tikz_canvas.rs b/crates/tikzarium-app/src/tikz_canvas.rs index 704c026b..f9bbcbfc 100644 --- a/crates/tikzarium-app/src/tikz_canvas.rs +++ b/crates/tikzarium-app/src/tikz_canvas.rs @@ -2187,8 +2187,13 @@ mod tests { vec![egui::Event::PointerMoved(end)], width, ); - let (line, _) = - b42_studio_event_frame(&ctx, &mut session, &mut source, vec![primary(end, false)], width); + let (line, _) = b42_studio_event_frame( + &ctx, + &mut session, + &mut source, + vec![primary(end, false)], + width, + ); let line = line.expect("Studio pointer release must focus authored command"); let owned = tikz_authoring::owned(&source); assert_eq!(owned.len(), 1, "{tool:?}: missing or duplicate C command"); From 01b626edaa607e05c25d309725fc29c171590b93 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:44:55 +0300 Subject: [PATCH 24/52] fix(B42): make F1 on/off visible in contextual Studio dock and test real key frames --- crates/tikzarium-app/src/studio_ui.rs | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 3c3b3699..c87ce997 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -287,6 +287,22 @@ pub fn icon_button( response.on_hover_text(tooltip) } +/// F1 opens the same C-backed Help tab; toggling F1 off must visibly leave +/// that tab rather than only changing an invisible Session::help boolean. +pub fn route_studio_help_toggle( + was_enabled: bool, + is_enabled: bool, + dock: &mut StudioDock, + inspector_open: &mut bool, +) { + if !was_enabled && is_enabled { + *dock = StudioDock::Help; + *inspector_open = true; + } else if was_enabled && !is_enabled && *dock == StudioDock::Help { + *dock = StudioDock::Properties; + } +} + /// Narrow permanent rail: four *groups* rather than dozens of buttons. /// Clicking a group opens a floating palette; the code and PDF keep their size. pub fn show_group_rail( @@ -546,6 +562,53 @@ mod tests { ); assert_eq!(StudioDock::ALL.len(), 3); } + #[test] + fn real_f1_key_frames_toggle_visible_studio_dock_without_editing_tex() { + let ctx = egui::Context::default(); + let source = "% ручной CRLF\r\n\\myOpaqueMacro{unknown}\r\n".to_owned(); + let before = source.clone(); + let mut session = Session::default(); + session.reset(&source); + let mut dock = StudioDock::Properties; + let mut open = false; + let frame = |event: egui::Event, + session: &mut Session, + dock: &mut StudioDock, + open: &mut bool| { + let _ = ctx.run( + egui::RawInput { + events: vec![event], + ..Default::default() + }, + |ctx| { + let old = session.help; + crate::tikz_canvas::handle_help_shortcut(ctx, session); + route_studio_help_toggle(old, session.help, dock, open); + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("Исходник редактируем, PDF — внешний XeLaTeX"); + }); + }, + ); + }; + let key = |pressed: bool| egui::Event::Key { + key: egui::Key::F1, + physical_key: None, + pressed, + repeat: false, + modifiers: egui::Modifiers::default(), + }; + frame(key(true), &mut session, &mut dock, &mut open); + assert!(session.help); + assert_eq!(dock, StudioDock::Help); + assert!(open); + frame(key(false), &mut session, &mut dock, &mut open); + frame(key(true), &mut session, &mut dock, &mut open); + assert!(!session.help); + assert_eq!(dock, StudioDock::Properties); + assert_eq!(source, before); + assert!(!session.undo_available()); + } + #[test] fn palette_follows_actual_source_pane_resize_not_default_width() { // Simulated measured response rectangles from a resizable source panel. From 8faa2989bf038fd1d6679d37338c168664668746 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:45:08 +0300 Subject: [PATCH 25/52] fix(B42): restore Properties tab when F1 closes Studio contextual help --- crates/tikzarium-app/src/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index d24c1553..39b8ca4c 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1738,9 +1738,13 @@ impl eframe::App for TikzariumApp { if self.pending_open.is_none() { let had_help = self.authoring.help; tikz_canvas::handle_help_shortcut(ctx, &mut self.authoring); - if self.prefs.studio_mode && !had_help && self.authoring.help { - self.prefs.studio_dock = StudioDock::Help; - self.prefs.studio_inspector_open = true; + if self.prefs.studio_mode { + studio_ui::route_studio_help_toggle( + had_help, + self.authoring.help, + &mut self.prefs.studio_dock, + &mut self.prefs.studio_inspector_open, + ); } if self.workspace == WorkspaceMode::Split && let Some(line) = tikz_canvas::handle_visual_undo_shortcut( From d59be95b7c2fa343454d11aebe685c3505bc3658 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:45:22 +0300 Subject: [PATCH 26/52] docs(B42): specify F1 Studio toggle acceptance and source-integrity gate --- plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index 6d36b1e7..2a9def8f 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -62,3 +62,14 @@ B42 real-RawInput six-gesture test covers code patches independently. Acceptance still requires a real X11 resize/rail hide/reopen screenshot sequence plus six live Studio gestures, genuine source Ctrl+Z and PDF. Synthetic rectangles and `UNIT_EVENT` do not prove real resizable GUI. + +## B42 F1 Studio dock coherence + +In B42, the first F1 switches to the shared C Help tab, but the second F1 +previously only cleared `Session::help` while leaving the Help tab visually +unchanged. Keep the control honest: F1 on opens the inspector's Help tab; +F1 off returns to Properties when Help is selected. The existing B41 F1 +fallback for no active tool still applies. Reuse one C catalogue. +Cover real `egui::Context::run(RawInput)` F1 down/up/down with persisted +opaque TeX and no Undo entries. Independent actual desktop screenshots +must still verify visible tab movement, not just the unit event. From 88877297726b0d788108e995b4dbec3e4bb22d92 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 11:45:30 +0300 Subject: [PATCH 27/52] docs(B42): specify F1 Studio toggle acceptance and source-integrity gate --- insights/2026-11-02_B42_UX_EVIDENCE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index 489b9826..5b9f111c 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -61,3 +61,16 @@ source widths. App code commit `2909e4684c8743462ccf205adf51c1ee94ac0c8b`; helper/test commit `312ffd86d648ae0a99937b18735cf7d8de064304`. Full GUI manual resize verification: **NOT_RUN** until exact-head X11 artifact and screenshots are reviewed. Physical Win10/M1/M4: **NOT_RUN**. + +## F1 help-tab state mismatch + +Source inspection: Studio `update` reacted only to F1 false→true, switching +`prefs.studio_dock=Help`; a subsequent F1 true→false left the tab at Help +although `Session::help` was false. This would render the second shortcut +visually inert. Fixed in B-owned Studio UI and main via shared +`route_studio_help_toggle`, with real egui RawInput F1 press/release/press +test asserting Help→Properties and no source/Undo mutations. Commits +`01b626edaa607e05c25d309725fc29c171590b93` and +`8faa2989bf038fd1d6679d37338c168664668746`. +Exact-head CI and independent real X11 F1 screenshot remain separate gates; +do not report PASS before evidence is available. From f2273814aa160be5c0ec7b8779f9e4b7831ab6d7 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 12:29:08 +0300 Subject: [PATCH 28/52] fix(B42): resolve exact rustfmt block in Studio F1 event-frame test --- crates/tikzarium-app/src/studio_ui.rs | 36 +++++++++++++-------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index c87ce997..63703b04 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -571,25 +571,23 @@ mod tests { session.reset(&source); let mut dock = StudioDock::Properties; let mut open = false; - let frame = |event: egui::Event, - session: &mut Session, - dock: &mut StudioDock, - open: &mut bool| { - let _ = ctx.run( - egui::RawInput { - events: vec![event], - ..Default::default() - }, - |ctx| { - let old = session.help; - crate::tikz_canvas::handle_help_shortcut(ctx, session); - route_studio_help_toggle(old, session.help, dock, open); - egui::CentralPanel::default().show(ctx, |ui| { - ui.label("Исходник редактируем, PDF — внешний XeLaTeX"); - }); - }, - ); - }; + let frame = + |event: egui::Event, session: &mut Session, dock: &mut StudioDock, open: &mut bool| { + let _ = ctx.run( + egui::RawInput { + events: vec![event], + ..Default::default() + }, + |ctx| { + let old = session.help; + crate::tikz_canvas::handle_help_shortcut(ctx, session); + route_studio_help_toggle(old, session.help, dock, open); + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("Исходник редактируем, PDF — внешний XeLaTeX"); + }); + }, + ); + }; let key = |pressed: bool| egui::Event::Key { key: egui::Key::F1, physical_key: None, From f56b3e12c48ab095c859671b74a6698c71072c84 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 12:34:19 +0300 Subject: [PATCH 29/52] fix(B42): audit every embedded PDF font and Cyrillic extraction in user-server MiKTeX QA --- scripts/tools/qa_windows_server_user.ps1 | 78 ++++++++++++++++++++---- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/scripts/tools/qa_windows_server_user.ps1 b/scripts/tools/qa_windows_server_user.ps1 index 1c0428fb..32755188 100644 --- a/scripts/tools/qa_windows_server_user.ps1 +++ b/scripts/tools/qa_windows_server_user.ps1 @@ -8,7 +8,7 @@ $reportPath=Join-Path $OutputDir 'windows-server-qa.json' $report=[ordered]@{ schema_version=1;status='INCOMPLETE';environment='USER_WINDOWS_SERVER' physical_windows10='NOT_RUN';mac_m1_m4='NOT_RUN';interactive_gui='NOT_RUN' - cargo='NOT_RUN';font_audit='NOT_RUN';package_autoinstall='DISABLED_BY_FLAG' + cargo='NOT_RUN';font_audit='NOT_RUN';text_audit='NOT_RUN';raster='NOT_RUN';package_autoinstall='DISABLED_BY_FLAG' } $exit=1 try { @@ -71,17 +71,72 @@ try { } $report.source_sha256=$sourceHash.ToLowerInvariant() $report.pdf_sha256=(Get-FileHash $pdf -Algorithm SHA256).Hash.ToLowerInvariant() + # A single /yes/ anywhere in pdffonts is insufficient: each PDF font + # needs its own embedding verdict. pdftotext is an independent Cyrillic + # content check; neither means glyphs were visually accepted on a GUI. $fonts=Get-Command pdffonts -ErrorAction SilentlyContinue $info=Get-Command pdfinfo -ErrorAction SilentlyContinue if($fonts -and $info){ - $details=(& $info.Source $pdf | Out-String) - $fontsText=(& $fonts.Source $pdf | Out-String) - $details | Set-Content (Join-Path $OutputDir 'pdfinfo.txt') - $fontsText | Set-Content (Join-Path $OutputDir 'pdffonts.txt') - if($details -notmatch '(?m)^Pages:\s+1\s*$' -or $fontsText -notmatch '\byes\b'){ - throw 'PDF page count or embedded font verification failed' + $details=(& $info.Source $pdf 2>&1 | Out-String) + $infoExit=$LASTEXITCODE + $fontsText=(& $fonts.Source $pdf 2>&1 | Out-String) + $fontsExit=$LASTEXITCODE + $details | Set-Content -LiteralPath (Join-Path $OutputDir 'pdfinfo.txt') -Encoding utf8 + $fontsText | Set-Content -LiteralPath (Join-Path $OutputDir 'pdffonts.txt') -Encoding utf8 + if($infoExit -ne 0 -or $fontsExit -ne 0 -or + $details -notmatch '(?m)^Pages:\s+1\s*$'){ + throw 'PDF metadata or one-page contract failed' } - $report.font_audit='PASS' + $lines=@($fontsText -split '\r?\n' | Where-Object { $_.Trim() }) + if($lines.Count -lt 3 -or $lines[0] -notmatch '\bemb\b'){ + throw 'pdffonts output missing table header or font records' + } + $fontRows=@($lines | Select-Object -Skip 2) + if($fontRows.Count -eq 0){throw 'PDF has no font records'} + foreach($row in $fontRows){ + $match=[regex]::Match($row, + '\s+(yes|no)\s+(yes|no)\s+(yes|no)\s+\d+\s+\d+\s*$', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + if(-not $match.Success){throw "Unparseable pdffonts record: $row"} + if($match.Groups[1].Value -ine 'yes'){ + throw "Nonembedded PDF font: $row" + } + } + if(($fontRows -join ' ') -notmatch '(?i)DejaVu.?Serif'){ + throw 'Cyrillic fixture did not embed the expected DejaVu Serif family' + } + $report.font_count=$fontRows.Count + $report.font_audit='PASS_ALL_FONT_ROWS_EMBEDDED' + }else{ + $report.font_audit='NOT_RUN_PDFINFO_OR_PDFFONTS_MISSING' + } + $extractor=Get-Command pdftotext -ErrorAction SilentlyContinue + if($extractor){ + $extracted=Join-Path $OutputDir 'pdftotext.txt' + & $extractor.Source '-enc' 'UTF-8' $pdf $extracted + if($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $extracted)){ + throw 'pdftotext extraction failed' + } + $actualText=Get-Content -LiteralPath $extracted -Raw -Encoding utf8 + foreach($expected in @('Тест XeLaTeX','Кириллица','Точка')){ + if(-not $actualText.Contains($expected)){ + throw "Expected Cyrillic content absent in PDF extraction: $expected" + } + } + $report.text_audit='PASS_EXPECTED_CYRILLIC_EXTRACTED' + }else{ + $report.text_audit='NOT_RUN_PDFTOTEXT_MISSING' + } + $rasterizer=Get-Command pdftoppm -ErrorAction SilentlyContinue + if($rasterizer){ + $prefix=Join-Path $OutputDir 'cyrillic-render' + & $rasterizer.Source '-f' '1' '-l' '1' '-singlefile' '-png' '-r' '144' $pdf $prefix + if($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath ($prefix+'.png'))){ + throw 'PDF raster generation failed' + } + $report.raster='GENERATED_REQUIRES_MANUAL_GLYPH_REVIEW' + }else{ + $report.raster='NOT_RUN_PDFTOPPM_MISSING' } $after=@(Inventory (Join-Path $OutputDir 'packages-after.txt')) if($before.Count -ne $after.Count -or (Compare-Object $before $after)){ @@ -100,9 +155,10 @@ try { $report.cargo='PASS_WINDOWS_SERVER_NOT_WIN10' } } - $report.status=if($report.font_audit -eq 'PASS'){ - 'PASS_USER_WINDOWS_SERVER_TEX_AND_PDF' - }else{'PARTIAL_PDF_FONTS_NOT_RUN'} + $report.status=if($report.font_audit -eq 'PASS_ALL_FONT_ROWS_EMBEDDED' -and + $report.text_audit -eq 'PASS_EXPECTED_CYRILLIC_EXTRACTED'){ + 'PASS_USER_WINDOWS_SERVER_TEX_FONT_AND_EXTRACTED_TEXT' + }else{'PARTIAL_PDF_FONT_OR_TEXT_AUDIT_NOT_RUN'} $exit=0 }finally{Pop-Location} }catch{ From 4b7eb68de52cce8de1672bfbc44eb6f6b086fe2f Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 12:34:50 +0300 Subject: [PATCH 30/52] docs(B42): record exact CI diagnosis, reviewed X11 evidence and Windows Server audit gates --- insights/2026-11-02_B42_UX_EVIDENCE.md | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index 5b9f111c..dce70e18 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -74,3 +74,46 @@ test asserting Help→Properties and no source/Undo mutations. Commits `8faa2989bf038fd1d6679d37338c168664668746`. Exact-head CI and independent real X11 F1 screenshot remain separate gates; do not report PASS before evidence is available. + +## B42 exact-head CI failure repaired and real Studio screenshots independently inspected + +On source SHA `88877297726b0d788108e995b4dbec3e4bb22d92`, strict Rust +formatting failed on every Rust matrix at `studio_ui.rs:571`; tests were +**not executed** past that gate. The exact rustfmt layout from the hosted +job log was applied in commit +`f2273814aa160be5c0ec7b8779f9e4b7831ab6d7`, on the SAME #91 branch. +Do not inherit earlier green tests; rerun all exact-head checks. + +Downloaded and inspected **two** real Ubuntu/X11 screenshots from the B42 +bootstrap [run 41421044602](https://github.com/i-shutov/tikzarium/actions/runs/41421044602) +on that formatted app tree. ZIP CRC passes and image SHA-256 matches the +artifact manifest: workspace +`b5f717856087c397b9a87a98965f581a180e6f055256724072a240f413538cad`; +Geometry popup +`d3be3595ba89bed353a8af1a7c11a81c173ac4140722c2e1edc213e92a1e3c77`. +The screenshot does visibly show the popup with four selectable geometric +icons outside the editable source pane. This only proves an actual +*group click*, not selection of an individual native tool, six gestures, +source manual Ctrl+Z, file Open/Save, or XeLaTeX PDF in the Studio process. +The artifact's originally stored manual-review field remains historically +NOT_RUN; this independent review is recorded here without falsifying it. + +Three older I51/I57 workflows on the formatted HEAD refuse the B42 tree +**before GUI execution**; those failures cannot count as gesture FAIL/PASS. +D-owned independent coordinate re-leasing remains mandatory. Physical +Windows 10 and M1/M4 remain NOT_RUN. + +## Persistent Windows Server PDF QA trust correction + +The proposed self-hosted audit incorrectly accepted +`pdffonts | -match '\byes\b'`, which only checked that *some* font had +`yes`. The B-owned script now parses every font record, rejects any +nonembedded row, requires DejaVu Serif in the Cyrillic fixture, checks +one page and extracts the expected Russian text with `pdftotext`. +Absent tools produce partial/NOT_RUN rather than false PASS. Optional +`pdftoppm` exports a PNG explicitly marked unreviewed, not proof of +visible glyph fidelity. Implemented in +`f56b3e12c48ab095c859671b74a6698c71072c84`. +This script is NOT_RUN on the actual dedicated Windows Server; hosted +MiKTeX proof does not substitute for it. Exact-head CI, script runtime, +and visual glyph review are independent gates. From 3fcb4adb827767c265c80b41c01b41b008417116 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 12:34:54 +0300 Subject: [PATCH 31/52] docs(B42): record exact CI diagnosis, reviewed X11 evidence and Windows Server audit gates --- ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index 2a9def8f..fe3ad0d0 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -73,3 +73,23 @@ fallback for no active tool still applies. Reuse one C catalogue. Cover real `egui::Context::run(RawInput)` F1 down/up/down with persisted opaque TeX and no Undo entries. Independent actual desktop screenshots must still verify visible tab movement, not just the unit event. + +## Reacceptance on B42 exact-head after CI and QA corrections + +The early B42 exact HEAD `88877297726b0d788108e995b4dbec3e4bb22d92` +failed `cargo fmt --all -- --check` on a single event-frame closure. +The fix `f2273814aa160be5c0ec7b8779f9e4b7831ab6d7` is production +semantics-neutral. Do not merge based on the earlier short-circuited tests; +require new successful hosted Rust/test/Clippy and D QA on the final SHA. + +The old B41/B37 X11 drivers have source-tree guards; D owns new Studio +calibration, six actual tool selections/gestures, inspector/Undo, F1, +manual source Ctrl+Z, two CRLF/opaque-macro Open/Save files, and PDF raster. +B bootstrap confirmed only the Geometry group popup on real X11. + +User-server audit must distinguish all embedded fonts, extracted expected +Cyrillic text, and visually reviewed rendered glyphs as separate facts. +The prior any-`yes` predicate was unsafe. The corrected B-owned +`scripts/tools/qa_windows_server_user.ps1` does not auto-install packages, +and produces partial states if Poppler PDF tools are unavailable. The +dedicated Windows Server and physical Windows 10/M1/M4 remain NOT_RUN. From 061c0cef32deb25a8398847a3da93da4e1697faa Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 12:35:28 +0300 Subject: [PATCH 32/52] test(B42): parse dedicated Windows Server QA script under hosted PowerShell without claiming runtime --- .github/workflows/b42-studio-x11.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/b42-studio-x11.yml b/.github/workflows/b42-studio-x11.yml index ed11bdef..bd9dc6ed 100644 --- a/.github/workflows/b42-studio-x11.yml +++ b/.github/workflows/b42-studio-x11.yml @@ -27,6 +27,22 @@ jobs: xvfb xauth openbox xdotool imagemagick libgl1-mesa-dri libglx-mesa0 \ libxkbcommon-dev libxkbcommon-x11-dev libx11-dev libxi-dev \ libxcursor-dev libxrandr-dev libudev-dev + - name: Parse user-server PowerShell audit without running it + shell: bash + run: | + # This only checks syntax on the hosted Linux runner. + # It does not run MiKTeX or certify the dedicated Windows Server. + pwsh -NoProfile -NonInteractive -Command ' + $tokens=$null; $errors=$null + [System.Management.Automation.Language.Parser]::ParseFile( + "scripts/tools/qa_windows_server_user.ps1", + [ref]$tokens, [ref]$errors) | Out-Null + if($errors.Count -ne 0) { + $errors | ForEach-Object { Write-Error $_ } + exit 1 + } + Write-Host "PowerShell AST syntax PASS; Windows Server runtime NOT_RUN" + ' - name: Compile the actual Rust/egui executable from locked sources run: | cargo build --locked -p tikzarium-app --bin tikzarium-app From dadd05b16c0c917f28a57f233b12931fbbcb98cd Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 12:37:51 +0300 Subject: [PATCH 33/52] fix(B42): fail closed on partial user-server font/text or requested Rust QA --- scripts/tools/qa_windows_server_user.ps1 | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/tools/qa_windows_server_user.ps1 b/scripts/tools/qa_windows_server_user.ps1 index 32755188..74e4d35b 100644 --- a/scripts/tools/qa_windows_server_user.ps1 +++ b/scripts/tools/qa_windows_server_user.ps1 @@ -155,11 +155,14 @@ try { $report.cargo='PASS_WINDOWS_SERVER_NOT_WIN10' } } - $report.status=if($report.font_audit -eq 'PASS_ALL_FONT_ROWS_EMBEDDED' -and - $report.text_audit -eq 'PASS_EXPECTED_CYRILLIC_EXTRACTED'){ + $complete=($report.font_audit -eq 'PASS_ALL_FONT_ROWS_EMBEDDED' -and + $report.text_audit -eq 'PASS_EXPECTED_CYRILLIC_EXTRACTED' -and + (-not $CheckRust -or $report.cargo -eq 'PASS_WINDOWS_SERVER_NOT_WIN10')) + $report.status=if($complete){ 'PASS_USER_WINDOWS_SERVER_TEX_FONT_AND_EXTRACTED_TEXT' - }else{'PARTIAL_PDF_FONT_OR_TEXT_AUDIT_NOT_RUN'} - $exit=0 + }else{'PARTIAL_REQUIRED_FONT_TEXT_OR_RUST_CHECK_NOT_RUN'} + # A PARTIAL report must not make the self-hosted workflow green. + $exit=if($complete){0}else{2} }finally{Pop-Location} }catch{ $report.status='FAIL' From be22ecef19299279e3b5dd34d1473aad89100ef3 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 13:23:24 +0300 Subject: [PATCH 34/52] fix(B42): make scratch Save destination real and cover egui Save event Root cause from I61 real-X11 artifact: figure.tex was a grey hint while Linux Save rejected an empty file field. Prefill safe non-overwriting name, test actual Save icon, CRLF/Unicode preservation and occupied-file guard; update B plan/insights. --- crates/tikzarium-app/src/main.rs | 96 ++++++++++++++++++- insights/2026-11-02_B42_UX_EVIDENCE.md | 21 ++++ ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 29 ++++++ 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 39b8ca4c..dc5cf00a 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -36,6 +36,10 @@ use studio_ui::{StudioDock, StudioGroup, StudioIcon}; use ui_theme::{STORAGE_KEY, UiPreferences}; const QUICKSTART: &str = include_str!("../../../examples/quickstart.tex"); +// The Linux Save action cannot use a native dialog: this is an actual, editable +// initial destination, never just a hint. typed_save_destination still forbids +// overwriting a pre-existing file without a deliberate path change. +const DEFAULT_SCRATCH_FILENAME: &str = "figure.tex"; // Fixed 500pt source + 250pt inspector left the exact PDF/guide unusable // at the native 850pt minimum window width. Reserve central space first. @@ -496,7 +500,7 @@ impl TikzariumApp { let package_statuses = registry.status(&tex); // cache; never spawn kpsewhich every frame Self { document: Document::scratch(QUICKSTART), - file_field: String::new(), + file_field: DEFAULT_SCRATCH_FILENAME.to_owned(), tex, package_statuses, diagnostics: Vec::new(), @@ -2827,6 +2831,96 @@ mod split_layout_tests { } } + #[test] + fn b42_studio_save_icon_event_persists_default_utf8_scratch_without_overwriting() { + // The exact production icon handler is driven by egui pointer frames. + // This is UNIT_EVENT, not an actual X11/Win10/macOS desktop run. + fn frame( + ctx: &egui::Context, + document: &mut Document, + destination: &Path, + rect: &mut egui::Rect, + events: Vec, + ) -> bool { + let mut saved = false; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(850.0, 560.0), + )), + events, + ..Default::default() + }, + |ctx| { + egui::TopBottomPanel::top("b42-save-icon-event").show(ctx, |ui| { + ui.horizontal(|ui| { + let response = studio_ui::icon_button( + ui, + StudioIcon::Save, + false, + 34.0, + "Сохранить исходник · Ctrl+S", + ); + *rect = response.rect; + if response.clicked() { + let path = typed_save_destination( + document, + destination.to_str().unwrap(), + ) + .unwrap(); + document.save_to(&path).unwrap(); + saved = true; + } + }); + }); + }, + ); + saved + } + + assert_eq!(DEFAULT_SCRATCH_FILENAME, "figure.tex"); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join(DEFAULT_SCRATCH_FILENAME); + let original = "% исходный комментарий\r\n\\MyOpaque{Не менять}\r\n"; + let mut document = Document::scratch(original); + let ctx = egui::Context::default(); + let mut rect = egui::Rect::NOTHING; + assert!(!frame(&ctx, &mut document, &path, &mut rect, vec![])); + assert!(rect.is_positive()); + let pos = rect.center(); + let button = |pressed| egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed, + modifiers: Default::default(), + }; + assert!(!frame( + &ctx, + &mut document, + &path, + &mut rect, + vec![egui::Event::PointerMoved(pos), button(true)], + )); + assert!(frame( + &ctx, + &mut document, + &path, + &mut rect, + vec![button(false)], + )); + assert_eq!(std::fs::read(&path).unwrap(), original.as_bytes()); + assert_eq!(Document::load(&path).unwrap().source, original); + assert!(!document.dirty()); + + let another_scratch = Document::scratch("% другой черновик\r\n"); + assert!( + typed_save_destination(&another_scratch, path.to_str().unwrap()).is_err(), + "the suggested scratch name must never overwrite a prior document" + ); + assert_eq!(std::fs::read(&path).unwrap(), original.as_bytes()); + } + #[test] fn b33_typed_save_rejects_existing_open_target_and_accepts_new_utf8_file() { let dir = tempfile::tempdir().unwrap(); diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index dce70e18..ea9861f9 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -117,3 +117,24 @@ visible glyph fidelity. Implemented in This script is NOT_RUN on the actual dedicated Windows Server; hosted MiKTeX proof does not substitute for it. Exact-head CI, script runtime, and visual glyph review are independent gates. + +## B42 external D I61 failed at first Save: screenshot-to-code root cause + +[run 41427351166](https://github.com/i-shutov/tikzarium/actions/runs/41427351166) +on #92 exact D SHA `517906e80fab2bda3c6aafa834a7b90a066790df` +returned `gui_save=NOT_RUN`, `six_real_tools=NOT_RUN`, +`real_xelatex_pdf=NOT_RUN`; error: `Save did not create default +figure.tex`. Independently downloaded GitHub artifact `13133090660`, +verified the screenshot bytes against its report and visually examined +`02-file-menu-default-path.png` and `04-source-pasted.png`. The +visible `figure.tex` was grey *placeholder*, not a value. Source +`TikzariumApp::new` set `file_field` empty, while Linux +`typed_save_destination` refuses empty input. This establishes +a real first-save workflow defect, not a gesture or XeLaTeX regression. + +Changed B main.rs only for production: the new scratch filename +`figure.tex` is prefilled as actual editable text, and existing +non-overwrite protection remains. Added actual egui event-frame +Save-icon test with CRLF/Cyrillic macro Save/Reopen and occupied +destination rejection. Exact-head hosted CI and new independent +D X11 acceptance remain PENDING; no physical OS claims. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index fe3ad0d0..c18d71f8 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -93,3 +93,32 @@ The prior any-`yes` predicate was unsafe. The corrected B-owned `scripts/tools/qa_windows_server_user.ps1` does not auto-install packages, and produces partial states if Poppler PDF tools are unavailable. The dedicated Windows Server and physical Windows 10/M1/M4 remain NOT_RUN. + +## B42 real-X11 scratch Save incident and B-owned correction (2026-11-02) + +Independent stacked D #92 I61 Linux/X11 on B42 source tree +`817ef8606d882ed39111b886fce57e0c0a37fb3a`, run +[41427351166](https://github.com/i-shutov/tikzarium/actions/runs/41427351166), +artifact `13133090660` stopped **after a real clipboard paste** with +`Studio toolbar Save did not create default figure.tex`. The original +`02-file-menu-default-path.png` shows that `figure.tex` was only a +TextEdit grey *hint*, whereas B-owned `TikzariumApp::new` initialized +`file_field = String::new()`. On Linux `save_as()` uses the actual +field via `typed_save_destination` and thus rejects an empty path. +This is a concrete Studio UX defect (and a wrong D assumption), **not** +a failure of the six native pointer tools: I61 never got that far. + +B42 now gives the new scratch file a **real editable** `figure.tex` +destination, matching the already displayed hint. Explicit user Save +writes the current directory only if that name does not exist; existing +files are still rejected by `typed_save_destination` (no overwrite). +File → Save As retains user-controlled paths. A new B-owned +`RawInput` UNIT_EVENT test clicks the *production vector Save icon*, +checks UTF-8/CRLF/opaque macro byte fidelity on disk and fails closed +for a second scratch with an occupied destination. + +New B-only commit HEAD exact SHA/CI: inspect after push; do not +report I61 PASS until D independently re-leases its app tree, +reruns all six real X11 icons/gestures, Save/Reopen, Help/Undo and +XeLaTeX source/PDF + manual artifact review. This UNIT_EVENT is +not physical Win10/MiKTeX, Mac M1/M4 or the user Windows Server. From ba1df560a176695cb88d4f6772f27618344718b7 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 13:25:48 +0300 Subject: [PATCH 35/52] style(B42): match exact hosted rustfmt in Studio Save event test --- crates/tikzarium-app/src/main.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index dc5cf00a..b69156a5 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -2864,11 +2864,9 @@ mod split_layout_tests { ); *rect = response.rect; if response.clicked() { - let path = typed_save_destination( - document, - destination.to_str().unwrap(), - ) - .unwrap(); + let path = + typed_save_destination(document, destination.to_str().unwrap()) + .unwrap(); document.save_to(&path).unwrap(); saved = true; } From ac8f6096fdf7ce1e2869251faff4039c6daf3642 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 14:20:43 +0300 Subject: [PATCH 36/52] test(B42): drive native Studio rail and six C tool icons with egui events Add exact-handler click verification with source/Undo invariants and B plans/insights in existing PR #91. --- crates/tikzarium-app/src/studio_ui.rs | 170 +++++++++++++++++- insights/2026-11-02_B42_UX_EVIDENCE.md | 13 ++ ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 15 ++ 3 files changed, 194 insertions(+), 4 deletions(-) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 63703b04..746d800e 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -309,6 +309,16 @@ pub fn show_group_rail( ui: &mut egui::Ui, active: &mut Option, narrow: bool, +) -> Option { + show_group_rail_observed(ui, active, narrow, |_, _| {}) +} + +// Observe native widget rectangles in event tests without changing production clicks. +fn show_group_rail_observed( + ui: &mut egui::Ui, + active: &mut Option, + narrow: bool, + mut observe: impl FnMut(StudioGroup, Rect), ) -> Option { let _ = narrow; ui.vertical_centered(|ui| { @@ -325,6 +335,7 @@ pub fn show_group_rail( 43.0, &tooltip, ); + observe(group, response.rect); if response.clicked() { *active = if *active == Some(group) { None @@ -361,6 +372,19 @@ pub fn show_palette( session: &mut Session, data: PaletteData<'_>, open_libraries: &mut bool, +) { + show_palette_observed(ctx, group, anchor, session, data, open_libraries, |_, _| {}); +} + +// The observer exposes the very same C tool buttons used by the Studio GUI. +fn show_palette_observed( + ctx: &egui::Context, + group: &mut Option, + anchor: Pos2, + session: &mut Session, + data: PaletteData<'_>, + open_libraries: &mut bool, + mut observe: impl FnMut(Tool, Rect), ) { let PaletteData { source, @@ -448,15 +472,15 @@ pub fn show_palette( info.manual_section, status ); - if icon_button( + let response = icon_button( ui, StudioIcon::Tool(tool), session.active == Some(tool), 46.0, &tip, - ) - .clicked() - { + ); + observe(tool, response.rect); + if response.clicked() { session.select_tool(tool); *group = None; } @@ -642,4 +666,142 @@ mod tests { assert_eq!(source, before); source.clear(); } + + #[test] + fn b42_rawinput_group_and_six_native_tool_icons_use_production_handlers() { + // Actual egui press/release; layout positions are observed from + // production widgets, not guessed or supplied as alternate controls. + // UNIT_EVENT only: does not replace real Linux/X11 desktop acceptance. + struct State { + group: Option, + anchor: Pos2, + groups: [Rect; 4], + tools: [Rect; 6], + session: Session, + filter: String, + libraries: bool, + } + impl State { + fn frame(&mut self, ctx: &egui::Context, source: &str, events: Vec) { + let State { + group, + anchor, + groups, + tools, + session, + filter, + libraries, + } = self; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(Rect::from_min_size( + Pos2::ZERO, + Vec2::new(850.0, 560.0), + )), + events, + ..Default::default() + }, + |ctx| { + egui::SidePanel::left("b42-real-event-rail") + .resizable(false) + .exact_width(62.0) + .show(ctx, |ui| { + if let Some(next) = + show_group_rail_observed(ui, group, true, |kind, rect| { + let index = StudioGroup::ALL + .iter() + .position(|&candidate| candidate == kind) + .unwrap(); + groups[index] = rect; + }) + { + *anchor = next; + } + }); + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("TikZ source is authoritative, not provisional overlay."); + }); + show_palette_observed( + ctx, + group, + *anchor, + session, + PaletteData { + source, + extensions: &[], + filter, + }, + libraries, + |tool, rect| { + let index = crate::tikz_authoring::TOOLS + .iter() + .position(|&candidate| candidate == tool) + .unwrap(); + tools[index] = rect; + }, + ); + }, + ); + } + fn click(&mut self, ctx: &egui::Context, source: &str, pos: Pos2) { + for pressed in [true, false] { + self.frame( + ctx, + source, + vec![ + egui::Event::PointerMoved(pos), + egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed, + modifiers: Default::default(), + }, + ], + ); + } + } + } + + let ctx = egui::Context::default(); + let source = "% русский комментарий\r\n\\myOpaque{Не менять}\r\n".to_owned(); + let untouched = source.clone(); + let mut state = State { + group: None, + anchor: Pos2::ZERO, + groups: [Rect::NOTHING; 4], + tools: [Rect::NOTHING; 6], + session: Session::default(), + filter: String::new(), + libraries: false, + }; + state.session.reset(&source); + for kind in [ + StudioGroup::Geometry, + StudioGroup::Connections, + StudioGroup::Text, + ] { + for &tool in kind.tools() { + state.frame(&ctx, &source, vec![]); + let group_index = StudioGroup::ALL + .iter() + .position(|&candidate| candidate == kind) + .unwrap(); + assert!(state.groups[group_index].is_positive()); + state.click(&ctx, &source, state.groups[group_index].center()); + assert_eq!(state.group, Some(kind), "{kind:?} group not opened"); + let tool_index = crate::tikz_authoring::TOOLS + .iter() + .position(|&candidate| candidate == tool) + .unwrap(); + assert!(state.tools[tool_index].is_positive(), "{tool:?} button absent"); + state.click(&ctx, &source, state.tools[tool_index].center()); + assert_eq!(state.session.active, Some(tool), "{tool:?} not selected"); + assert_eq!(state.group, None, "palette remains open after tool click"); + assert_eq!(source, untouched, "click cannot rewrite opaque TeX"); + assert!(!state.session.undo_available(), "selection is not a patch"); + } + } + assert!(!state.libraries); + } + } diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index ea9861f9..f39ce316 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -138,3 +138,16 @@ non-overwrite protection remains. Added actual egui event-frame Save-icon test with CRLF/Cyrillic macro Save/Reopen and occupied destination rejection. Exact-head hosted CI and new independent D X11 acceptance remain PENDING; no physical OS claims. + + +## B42 actual group/tool selection event-frame QA — 2026-11-02 + +Identified an evidence gap, not a confirmed new production UX bug: +the previously added six-gesture Studio test directly sets Session::active, +and X11 bootstrap proves group opening only. The new B-owned UNIT_EVENT +uses production rail/palette click handlers, egui Response rectangles and +pointer press/release for all six C-catalogue native tool icons. Selection +closes the palette without editing CRLF, Cyrillic text, unknown macros or Undo. + +Exact-head Rust test and independent X11 acceptance are pending after +this patch. Never equate the event test with physical Win10/M1/M4. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index c18d71f8..675e0d1e 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -122,3 +122,18 @@ report I61 PASS until D independently re-leases its app tree, reruns all six real X11 icons/gestures, Save/Reopen, Help/Undo and XeLaTeX source/PDF + manual artifact review. This UNIT_EVENT is not physical Win10/MiKTeX, Mac M1/M4 or the user Windows Server. + + +## B42 grouped icon selection UNIT_EVENT extension — 2026-11-02 + +Gap found: existing six-gesture Studio tests inject an active tool, while +real Linux/X11 bootstrap only proved opening the Geometry group. The B +egui regression now clicks real group and all six tool Response handlers +via press/release RawInput frames at 850×560. Actual egui response rectangles +are observed; production publicly exposed signatures remain unchanged. +The native C registry still supplies tools, syntax and generator. + +The selected tool, popup dismissal, and no source/Undo modification are +asserted. This is UNIT_EVENT, not a physical desktop test, not PDF validation. +D's independent I61 real X11, manual source Ctrl+Z, two-file Open/Save, and +external XeLaTeX remain necessary. Await exact-head CI results. From cfa0d8dda4e07293b66aac1b4982f5026bca9e1a Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 14:21:49 +0300 Subject: [PATCH 37/52] fix(B42): match hosted rustfmt diagnostic for native palette event test --- crates/tikzarium-app/src/studio_ui.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 746d800e..ea5da498 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -694,10 +694,7 @@ mod tests { } = self; let _ = ctx.run( egui::RawInput { - screen_rect: Some(Rect::from_min_size( - Pos2::ZERO, - Vec2::new(850.0, 560.0), - )), + screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(850.0, 560.0))), events, ..Default::default() }, @@ -793,7 +790,10 @@ mod tests { .iter() .position(|&candidate| candidate == tool) .unwrap(); - assert!(state.tools[tool_index].is_positive(), "{tool:?} button absent"); + assert!( + state.tools[tool_index].is_positive(), + "{tool:?} button absent" + ); state.click(&ctx, &source, state.tools[tool_index].center()); assert_eq!(state.session.active, Some(tool), "{tool:?} not selected"); assert_eq!(state.group, None, "palette remains open after tool click"); @@ -803,5 +803,4 @@ mod tests { } assert!(!state.libraries); } - } From 1aa79bb8112997b3907089a965d6dedccc375f5a Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 14:26:27 +0300 Subject: [PATCH 38/52] fix(B42): reserve full-width status bar before source and guide panels Fix order-dependent egui bottom row overlap; test real RawInput panel layout at 850/1366 and document QA boundaries. --- crates/tikzarium-app/src/main.rs | 122 ++++++++++++++++-- insights/2026-11-02_B42_UX_EVIDENCE.md | 15 +++ ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 17 +++ 3 files changed, 142 insertions(+), 12 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index b69156a5..abea8c62 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1726,6 +1726,30 @@ impl TikzariumApp { } } +/// Reserve the Studio status row before any side/central panel consumes the +/// remaining viewport. The bottom bar is full width, and on-demand diagnostics +/// stack ABOVE it; editable source and provisional guide cannot render beneath +/// an after-the-fact CentralPanel overlay. +fn show_studio_status_panel( + ctx: &egui::Context, + status: &str, + xelatex_ready: bool, +) -> egui::Rect { + egui::TopBottomPanel::bottom("b42-status") + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new(status).small()); + if xelatex_ready { + ui.small("XeLaTeX: настроен"); + } else { + ui.colored_label(ui.visuals().warn_fg_color, "XeLaTeX не выбран"); + } + }); + }) + .response + .rect +} + impl eframe::App for TikzariumApp { fn save(&mut self, storage: &mut dyn eframe::Storage) { eframe::set_value(storage, STORAGE_KEY, &self.prefs); @@ -1884,6 +1908,9 @@ impl eframe::App for TikzariumApp { }); } let split_layout = SplitPanelLayout::for_width(ctx.available_rect().width()); + if self.prefs.studio_mode { + show_studio_status_panel(ctx, &self.status, self.tex.xelatex.is_some()); + } if self.prefs.studio_mode && self.prefs.studio_inspector_open { let studio = studio_ui::StudioLayout::for_width(ctx.available_rect().width()); egui::SidePanel::right("b42-studio-inspector") @@ -2063,18 +2090,6 @@ impl eframe::App for TikzariumApp { self.prefs.studio_inspector_open = true; } } - if self.prefs.studio_mode { - egui::TopBottomPanel::bottom("b42-status").show(ctx, |ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new(&self.status).small()); - if self.tex.xelatex.is_none() { - ui.colored_label(ui.visuals().warn_fg_color, "XeLaTeX не выбран"); - } else { - ui.small("XeLaTeX: настроен"); - } - }); - }); - } if let Some(path) = self.pending_open.clone() { let previous_destination = self.pending_open_save_path.clone(); let decision = show_pending_open_dialog( @@ -2831,6 +2846,89 @@ mod split_layout_tests { } } + + #[test] + fn b42_status_is_full_width_and_does_not_occlude_editable_source_or_guide() { + // The production status panel must be allocated BEFORE any inspector, + // source SidePanel or CentralPanel, at minimum and normal viewport. + // RawInput layout evidence only, not a physical OS-window screenshot. + for (width, height) in [(850.0, 560.0), (1366.0, 768.0)] { + let ctx = egui::Context::default(); + let mut source = QUICKSTART.to_owned(); + let original = source.clone(); + let mut session = tikz_authoring::Session::default(); + session.reset(&source); + let mut status_rect = egui::Rect::NOTHING; + let mut source_rect = egui::Rect::NOTHING; + let mut guide_rect = egui::Rect::NOTHING; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(width, height), + )), + ..Default::default() + }, + |ctx| { + egui::TopBottomPanel::top("b42-status-test-toolbar").show(ctx, |ui| { + ui.label("Studio: source + native TikZ guide"); + }); + status_rect = show_studio_status_panel(ctx, "Сохраните рисунок", false); + egui::SidePanel::right("b42-status-test-inspector") + .exact_width(225.0) + .show(ctx, |ui| { + ui.label("Русские пояснения C"); + }); + egui::SidePanel::left("b42-status-test-tools") + .exact_width(62.0) + .show(ctx, |ui| { + ui.label("TikZ"); + }); + source_rect = show_split_source_panel( + ctx, + SplitPanelLayout::for_width(ctx.available_rect().width()), + |ui| { + ui.add(egui::TextEdit::multiline(&mut source).code_editor()); + }, + ); + guide_rect = egui::CentralPanel::default() + .show(ctx, |ui| { + tikz_canvas::show_guide_studio( + ui, + &mut session, + &mut source, + tikz_canvas::Capabilities { + installed: true, + loaded: Evidence::Confirmed, + engine_compatible: true, + implemented: true, + }, + ); + }) + .response + .rect; + }, + ); + assert!(status_rect.is_positive()); + assert!(source_rect.is_positive()); + assert!(guide_rect.is_positive()); + assert!( + status_rect.width() >= width - 2.0, + "status row not full width at {width}: {status_rect:?}" + ); + assert!( + source_rect.bottom() <= status_rect.top() + 1.0, + "source hidden under status at {width}" + ); + assert!( + guide_rect.bottom() <= status_rect.top() + 1.0, + "guide hidden under status at {width}" + ); + assert_eq!(source, original); + assert!(!session.undo_available()); + } + } + #[test] fn b42_studio_save_icon_event_persists_default_utf8_scratch_without_overwriting() { // The exact production icon handler is driven by egui pointer frames. diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index f39ce316..989fd6bf 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -151,3 +151,18 @@ closes the palette without editing CRLF, Cyrillic text, unknown macros or Undo. Exact-head Rust test and independent X11 acceptance are pending after this patch. Never equate the event test with physical Win10/M1/M4. + + +## B42 full-width status bar layout order — 2026-11-02 + +Reproducible source-level UX defect: eframe::App::update previously called +TopBottomPanel::bottom("b42-status") *after* CentralPanel and the side panels. +Egui layout is order-dependent; late bottom allocation can obscure content +and cannot reserve the intended full viewport width. The production call is +now before inspector, tool rail, source, central and optional diagnostics, +with the same status text and XeLaTeX prerequisite indication. + +B-owned UNIT_EVENT uses the same status handler and actual egui panel layout +at 850x560 / 1366x768 to assert full width, nonoverlapping source/guide, +unchanged TeX and Undo. No invented PDF coordinate authority. Hosted exact-head +CI and independent real X11 remain separate and pending at authoring. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index 675e0d1e..7a9787b5 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -137,3 +137,20 @@ The selected tool, popup dismissal, and no source/Undo modification are asserted. This is UNIT_EVENT, not a physical desktop test, not PDF validation. D's independent I61 real X11, manual source Ctrl+Z, two-file Open/Save, and external XeLaTeX remain necessary. Await exact-head CI results. + + +## B42 bottom-status panel structural layout repair — 2026-11-02 + +Code audit identified a concrete egui panel ordering issue: Studio rendered +TopBottomPanel::bottom("b42-status") AFTER CentralPanel had consumed the +viewport. That makes bottom-row reservation order-dependent and risks +overlapping/cropping the live guide or source. Also bottom status rendered +after side panels, so it did not reserve the full window width. + +Move the real status panel handler before all Studio side and central panels +(and before optional diagnostics) so it owns the full-width bottom row and +diagnostics sit above it. Add a real egui RawInput layout regression with +the production status handler, editor TextEdit and native guide at 850x560 +and 1366x768. Assert full width, status non-overlap, source byte integrity +and no spurious Undo. UNIT_EVENT, not physical Win10/M1/M4 or real X11. +Exact-head Rust/QA and independent desktop screenshots are acceptance gates. From 8792995135474dbe2cb13d79b0607a582868c078 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 14:29:28 +0300 Subject: [PATCH 39/52] fix(B42): apply exact hosted rustfmt diagnostics to Studio status test --- crates/tikzarium-app/src/main.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index abea8c62..8d47405b 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1730,11 +1730,7 @@ impl TikzariumApp { /// remaining viewport. The bottom bar is full width, and on-demand diagnostics /// stack ABOVE it; editable source and provisional guide cannot render beneath /// an after-the-fact CentralPanel overlay. -fn show_studio_status_panel( - ctx: &egui::Context, - status: &str, - xelatex_ready: bool, -) -> egui::Rect { +fn show_studio_status_panel(ctx: &egui::Context, status: &str, xelatex_ready: bool) -> egui::Rect { egui::TopBottomPanel::bottom("b42-status") .show(ctx, |ui| { ui.horizontal(|ui| { @@ -2846,7 +2842,6 @@ mod split_layout_tests { } } - #[test] fn b42_status_is_full_width_and_does_not_occlude_editable_source_or_guide() { // The production status panel must be allocated BEFORE any inspector, From ee6ba68b28c63dcad326f9bf3713e0d9cbc03a8c Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 14:31:22 +0300 Subject: [PATCH 40/52] test(B42): settle foreground palette hit testing after opening group --- crates/tikzarium-app/src/studio_ui.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index ea5da498..afa59f4b 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -786,6 +786,10 @@ mod tests { assert!(state.groups[group_index].is_positive()); state.click(&ctx, &source, state.groups[group_index].center()); assert_eq!(state.group, Some(kind), "{kind:?} group not opened"); + // egui Areas register their popup hit-test widgets on the + // following frame. Settle that frame before dispatching the + // next primary press into a newly opened foreground palette. + state.frame(&ctx, &source, vec![]); let tool_index = crate::tikz_authoring::TOOLS .iter() .position(|&candidate| candidate == tool) From c7f4aa33c5231ae681ca74f03a7f779cfbda0f1b Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 15:27:49 +0300 Subject: [PATCH 41/52] fix(B42): unify Studio Help toggle and safe dirty-Open scratch Save --- crates/tikzarium-app/src/main.rs | 126 ++++++++++++++-- crates/tikzarium-app/src/studio_ui.rs | 139 ++++++++++++++++++ insights/2026-11-02_B42_UX_EVIDENCE.md | 23 +++ ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 29 ++++ 4 files changed, 307 insertions(+), 10 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 8d47405b..7dd9c586 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -365,6 +365,25 @@ fn pending_open_save_destination( Ok(save_path) } +/// A dirty scratch must retain its own safe Save-and-open destination even +/// after the File menu field has been changed to the file being opened. +/// Never suggest a path that is occupied or aliases the incoming target. +fn initial_pending_open_save_path( + document: &Document, + open_target: &Path, + scratch_candidate: &Path, +) -> String { + let candidate = document.path.as_deref().unwrap_or(scratch_candidate); + let Some(text) = candidate.to_str() else { + return String::new(); + }; + if pending_open_save_destination(document, text, open_target).is_ok() { + text.to_owned() + } else { + String::new() + } +} + /// Real egui controls; returning the save-button rect additionally allows /// pointer event-frame tests to exercise the exact production click handler. fn pending_open_controls( @@ -565,12 +584,11 @@ impl TikzariumApp { } if self.document.dirty() { self.pending_open_validation_error = None; - self.pending_open_save_path = self - .document - .path - .as_ref() - .map(|current| current.display().to_string()) - .unwrap_or_default(); + self.pending_open_save_path = initial_pending_open_save_path( + &self.document, + &path, + Path::new(DEFAULT_SCRATCH_FILENAME), + ); self.pending_open = Some(path); return; } @@ -1222,15 +1240,18 @@ impl TikzariumApp { if studio_ui::icon_button( ui, StudioIcon::Help, - self.prefs.studio_dock == StudioDock::Help, + self.prefs.studio_dock == StudioDock::Help + && self.prefs.studio_inspector_open, 32.0, "Контекстная справка F1 из общего TikZ-каталога", ) .clicked() { - self.authoring.help = true; - self.prefs.studio_dock = StudioDock::Help; - self.prefs.studio_inspector_open = true; + studio_ui::toggle_studio_help_button( + &mut self.authoring, + &mut self.prefs.studio_dock, + &mut self.prefs.studio_inspector_open, + ); } if studio_ui::icon_button( ui, @@ -3104,6 +3125,91 @@ mod split_layout_tests { ); } + #[test] + fn b42_dirty_open_scratch_preserves_safe_destination_on_real_egui_save_click() { + // UNIT_EVENT: drive the production Save-and-open button. The File menu + // may have changed its field to the incoming file; never save there. + fn frame( + ctx: &egui::Context, + document: &Document, + target: &Path, + input: &mut String, + events: Vec, + ) -> (Option, egui::Rect) { + let mut result = (None, egui::Rect::NOTHING); + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(850.0, 560.0), + )), + events, + ..Default::default() + }, + |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + let (action, rect, _) = + pending_open_controls(ui, document, target, input, None); + result = (action, rect); + }); + }, + ); + result + } + + let dir = tempfile::tempdir().unwrap(); + let target = dir.path().join("открыть.tex"); + let safe = dir.path().join("мой-черновик.tex"); + let original = "% чужой CRLF\r\n\\foreignMacro{Не менять}\r\n"; + let scratch_source = "% набросок\r\n\\opaqueUser{оставить}\r\n"; + std::fs::write(&target, original).unwrap(); + let mut scratch = Document::scratch(scratch_source); + assert!(scratch.dirty()); + let mut input = initial_pending_open_save_path(&scratch, &target, &safe); + assert_eq!(input, safe.to_str().unwrap()); + let ctx = egui::Context::default(); + let (_, button) = frame(&ctx, &scratch, &target, &mut input, vec![]); + assert!(button.is_positive()); + let at = button.center(); + let pointer = |pressed| egui::Event::PointerButton { + pos: at, + button: egui::PointerButton::Primary, + pressed, + modifiers: Default::default(), + }; + frame( + &ctx, + &scratch, + &target, + &mut input, + vec![egui::Event::PointerMoved(at), pointer(true)], + ); + assert_eq!( + frame(&ctx, &scratch, &target, &mut input, vec![pointer(false)]).0, + Some(PendingOpenAction::Save(safe.clone())) + ); + scratch.save_to(&safe).unwrap(); + assert_eq!(std::fs::read(&safe).unwrap(), scratch_source.as_bytes()); + assert_eq!(Document::load(&target).unwrap().source, original); + assert_eq!(Document::load(&safe).unwrap().source, scratch_source); + assert_eq!( + initial_pending_open_save_path(&Document::scratch(scratch_source), &target, &safe), + "", + "occupied destination must not be suggested again" + ); + assert_eq!( + initial_pending_open_save_path(&Document::scratch(scratch_source), &target, &target), + "", + "incoming target must not be proposed as scratch Save" + ); + let mut loaded = Document::load(&target).unwrap(); + loaded.source.push_str("% ручная правка\r\n"); + assert_eq!( + initial_pending_open_save_path(&loaded, &target, &safe), + target.to_str().unwrap() + ); + } + #[test] fn b32_real_egui_pending_open_click_rejects_target_then_saves_elsewhere() { fn frame( diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index afa59f4b..923df6d4 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -303,6 +303,23 @@ pub fn route_studio_help_toggle( } } +/// The toolbar Help icon is a toggle, just like F1. A hidden inspector +/// with a remembered Help tab must reopen, not close. +pub fn toggle_studio_help_button( + session: &mut Session, + dock: &mut StudioDock, + inspector_open: &mut bool, +) { + if *inspector_open && *dock == StudioDock::Help { + session.help = false; + *dock = StudioDock::Properties; + } else { + session.help = true; + *dock = StudioDock::Help; + *inspector_open = true; + } +} + /// Narrow permanent rail: four *groups* rather than dozens of buttons. /// Clicking a group opens a floating palette; the code and PDF keep their size. pub fn show_group_rail( @@ -631,6 +648,128 @@ mod tests { assert!(!session.undo_available()); } + #[test] + fn b42_real_help_toolbar_pointer_and_f1_toggle_the_visible_dock() { + // UNIT_EVENT on the same production icon Response and F1 route. + fn frame( + ctx: &egui::Context, + session: &mut Session, + dock: &mut StudioDock, + open: &mut bool, + events: Vec, + ) -> Rect { + let mut rect = Rect::NOTHING; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(Rect::from_min_size( + Pos2::ZERO, + egui::vec2(850.0, 560.0), + )), + events, + ..Default::default() + }, + |ctx| { + let old = session.help; + crate::tikz_canvas::handle_help_shortcut(ctx, session); + route_studio_help_toggle(old, session.help, dock, open); + egui::TopBottomPanel::top("b42-help-click").show(ctx, |ui| { + let response = icon_button( + ui, + StudioIcon::Help, + *dock == StudioDock::Help && *open, + 32.0, + "Контекстная справка F1 из общего TikZ-каталога", + ); + rect = response.rect; + if response.clicked() { + toggle_studio_help_button(session, dock, open); + } + }); + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("Редактируемый TeX; точный PDF строит XeLaTeX"); + }); + }, + ); + rect + } + + let ctx = egui::Context::default(); + let source = "% русский CRLF\r\n\\opaqueMacro{не переписывать}\r\n"; + let mut session = Session::default(); + session.reset(source); + let mut dock = StudioDock::Properties; + let mut open = false; + let rect = frame(&ctx, &mut session, &mut dock, &mut open, vec![]); + assert!(rect.is_positive()); + let at = rect.center(); + let pointer = |pressed| egui::Event::PointerButton { + pos: at, + button: egui::PointerButton::Primary, + pressed, + modifiers: Default::default(), + }; + for visible in [true, false] { + frame( + &ctx, + &mut session, + &mut dock, + &mut open, + vec![egui::Event::PointerMoved(at), pointer(true)], + ); + frame( + &ctx, + &mut session, + &mut dock, + &mut open, + vec![pointer(false)], + ); + assert_eq!(session.help, visible); + assert_eq!( + dock, + if visible { + StudioDock::Help + } else { + StudioDock::Properties + } + ); + assert!(open); + } + dock = StudioDock::Help; + open = false; + frame( + &ctx, + &mut session, + &mut dock, + &mut open, + vec![egui::Event::PointerMoved(at), pointer(true)], + ); + frame( + &ctx, + &mut session, + &mut dock, + &mut open, + vec![pointer(false)], + ); + assert!(session.help && open && dock == StudioDock::Help); + + let key = |pressed| egui::Event::Key { + key: egui::Key::F1, + physical_key: None, + pressed, + repeat: false, + modifiers: Default::default(), + }; + frame(&ctx, &mut session, &mut dock, &mut open, vec![key(true)]); + assert!(!session.help); + assert_eq!(dock, StudioDock::Properties); + frame(&ctx, &mut session, &mut dock, &mut open, vec![key(false)]); + frame(&ctx, &mut session, &mut dock, &mut open, vec![key(true)]); + assert!(session.help); + assert_eq!(dock, StudioDock::Help); + assert_eq!(source, "% русский CRLF\r\n\\opaqueMacro{не переписывать}\r\n"); + assert!(!session.undo_available()); + } + #[test] fn palette_follows_actual_source_pane_resize_not_default_width() { // Simulated measured response rectangles from a resizable source panel. diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index 989fd6bf..1b7dd5ea 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -166,3 +166,26 @@ B-owned UNIT_EVENT uses the same status handler and actual egui panel layout at 850x560 / 1366x768 to assert full width, nonoverlapping source/guide, unchanged TeX and Undo. No invented PDF coordinate authority. Hosted exact-head CI and independent real X11 remain separate and pending at authoring. + + +## B42 Help icon and dirty-Open continuity QA (2026-11-02) + +Source audit isolated two concrete B-owned UX defects: + +1. In Studio, the Help toolbar handler always forced Help on, while + F1 already toggled the dock. Second icon click was visually inert. + A single B transition now toggles to Properties; when the inspector + is hidden with Help remembered, clicking reopens Help. The selected + icon now reflects actual inspector visibility. +2. Dirty Open initialized Save-and-open only from document.path. + For a dirty scratch this was empty despite B42 having a real + figure.tex scratch Save default. The new initial suggestion uses + the existing typed-save and open-target guards, never the incoming + File-menu target. Occupied, aliased or target candidates fail closed. + +Added direct egui RawInput click tests on production Help icon and modal +Save button, F1 interoperability, CRLF/Russian/unknown macro preservation +and no false visual Undo. These tests do not mean true desktop GUI +acceptance. D #92 Studio X11 six gestures, manual source Ctrl+Z, +two-file Save/Open and exact XeLaTeX PDF remain gates; physical +Windows10/M1/M4 and the owner's Windows Server remain NOT_RUN. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index 7a9787b5..34b7f2ab 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -154,3 +154,32 @@ the production status handler, editor TextEdit and native guide at 850x560 and 1366x768. Assert full width, status non-overlap, source byte integrity and no spurious Undo. UNIT_EVENT, not physical Win10/M1/M4 or real X11. Exact-head Rust/QA and independent desktop screenshots are acceptance gates. + + +## B42 Help toggle and dirty-Open scratch continuity (2026-11-02) + +Two B42 UX inconsistencies were reproduced directly in the production +event paths. The toolbar Help icon unconditionally set Help=true; a +second click on a selected Help icon did not leave the Help tab, unlike +the already supported F1 toggle. The B-owned toolbar handler now changes +the same visible Properties/Help dock, including reopening a hidden +inspector with a remembered Help tab. A real egui Context::run(RawInput) +press/release test covers two icon clicks, hidden dock, subsequent F1 +and preservation of Cyrillic/CRLF/opaque TeX and visual Undo. + +After the earlier first-Save figure.tex fix, dirty Open of an unsaved +scratch still initialized its independent Save-and-open path as empty. +The File menu path may already refer to the incoming document and must +not be reused. A B helper suggests the scratch filename only after +the pre-existing occupied-path and open-target checks succeed; if +occupied, aliased or equal to the incoming file it stays blank and +requires explicit user input. Already loaded documents retain their +current legitimate Save path. A real egui pointer test clicks the +existing production Save-and-open handler, verifies target preservation +and reopens both files byte-exactly. + +Only B files and these plans/insights are changed; no C generator, +A/D/E/F source, TeX/CAS/solver or PDF-to-coordinates. Classification: +UNIT_EVENT only, not physical Windows 10/M1/M4 or full Linux/X11. +#91 remains draft pending exact-head hosted fmt/test/Clippy and +independent D #92 real six-tool source-to-XeLaTeX acceptance. From 91e2d8201cd0c016e00d52618e4024fedb66424e Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 15:28:50 +0300 Subject: [PATCH 42/52] style(B42): apply exact hosted rustfmt diagnostics for Help event QA --- crates/tikzarium-app/src/main.rs | 3 +-- crates/tikzarium-app/src/studio_ui.rs | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 7dd9c586..9fa92165 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1240,8 +1240,7 @@ impl TikzariumApp { if studio_ui::icon_button( ui, StudioIcon::Help, - self.prefs.studio_dock == StudioDock::Help - && self.prefs.studio_inspector_open, + self.prefs.studio_dock == StudioDock::Help && self.prefs.studio_inspector_open, 32.0, "Контекстная справка F1 из общего TikZ-каталога", ) diff --git a/crates/tikzarium-app/src/studio_ui.rs b/crates/tikzarium-app/src/studio_ui.rs index 923df6d4..abe8924d 100644 --- a/crates/tikzarium-app/src/studio_ui.rs +++ b/crates/tikzarium-app/src/studio_ui.rs @@ -661,10 +661,7 @@ mod tests { let mut rect = Rect::NOTHING; let _ = ctx.run( egui::RawInput { - screen_rect: Some(Rect::from_min_size( - Pos2::ZERO, - egui::vec2(850.0, 560.0), - )), + screen_rect: Some(Rect::from_min_size(Pos2::ZERO, egui::vec2(850.0, 560.0))), events, ..Default::default() }, @@ -766,7 +763,10 @@ mod tests { frame(&ctx, &mut session, &mut dock, &mut open, vec![key(true)]); assert!(session.help); assert_eq!(dock, StudioDock::Help); - assert_eq!(source, "% русский CRLF\r\n\\opaqueMacro{не переписывать}\r\n"); + assert_eq!( + source, + "% русский CRLF\r\n\\opaqueMacro{не переписывать}\r\n" + ); assert!(!session.undo_available()); } From cc33930892fdecc915cc89678ecaf021b2a5bc6a Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 15:58:34 +0300 Subject: [PATCH 43/52] test(I61): rebase real Studio source-to-PDF QA onto frozen B42 and repair font audit --- .github/workflows/i61-b42-studio-x11.yml | 59 ++++ CHANGELOG.md | 6 + .../2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md | 30 ++ ...1-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md | 30 ++ scripts/qa_i61_b42_studio_x11.py | 296 ++++++++++++++++++ scripts/tests/test_qa_i61_studio_fonts.py | 33 ++ 6 files changed, 454 insertions(+) create mode 100644 .github/workflows/i61-b42-studio-x11.yml create mode 100644 insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md create mode 100644 plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md create mode 100644 scripts/qa_i61_b42_studio_x11.py create mode 100644 scripts/tests/test_qa_i61_studio_fonts.py diff --git a/.github/workflows/i61-b42-studio-x11.yml b/.github/workflows/i61-b42-studio-x11.yml new file mode 100644 index 00000000..ab1145cd --- /dev/null +++ b/.github/workflows/i61-b42-studio-x11.yml @@ -0,0 +1,59 @@ +name: I61 B42 real Studio icon-to-native-source/PDF (independent D QA) +on: + push: + branches: [qa/i61-b42-studio-source-pdf] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: i61-studio-${{ github.event.pull_request.head.sha || github.sha }} + cancel-in-progress: true +jobs: + studio-full-vertical: + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + CARGO_PROFILE_DEV_DEBUG: '0' + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec + with: + toolchain: '1.91.0' + - name: Exact B42 app source-tree lease (fail closed before GUI) + run: | + test "$(git rev-parse HEAD:crates/tikzarium-app/src)" = '64df229392c03a99ca29238bcc2e5f422c4d7409' + echo "TIKZARIUM_APP_TREE_SHA=$(git rev-parse HEAD:crates/tikzarium-app/src)" >> "$GITHUB_ENV" + echo "TIKZARIUM_SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + git diff --exit-code -- Cargo.lock + - name: Font-audit offline contract tests (NOT GUI) + run: python3 -m unittest discover -s scripts/tests -p test_qa_i61_studio_fonts.py -v + - name: Hosted Linux/X11 and external TeX Live XeLaTeX prerequisites + run: | + sudo apt-get update -q + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + xvfb xauth openbox xdotool xclip imagemagick libgl1-mesa-dri libglx-mesa0 \ + libxkbcommon-dev libxkbcommon-x11-dev libx11-dev libxi-dev \ + libxcursor-dev libxrandr-dev libudev-dev \ + texlive-xetex texlive-latex-extra texlive-pictures poppler-utils fonts-dejavu-core + xelatex --version | head -n 2 + command -v pdffonts + - name: Compile locked production egui binary, no QA stub + run: | + cargo build --locked -p tikzarium-app --bin tikzarium-app + git diff --exit-code -- Cargo.lock + - name: Real group icon, all six pointers, source Save and XeLaTeX PDF + run: | + mkdir -p "$RUNNER_TEMP/i61-runtime" + chmod 700 "$RUNNER_TEMP/i61-runtime" + export XDG_RUNTIME_DIR="$RUNNER_TEMP/i61-runtime" + xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX +render -noreset' \ + bash -c 'openbox --sm-disable >/tmp/i61-openbox.log 2>&1 & python3 scripts/qa_i61_b42_studio_x11.py --app "$GITHUB_WORKSPACE/target/debug/tikzarium-app" --out "$RUNNER_TEMP/i61-evidence"' + - name: Upload actual GUI screenshots, saved source, PDF, log and SHA ledger + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: i61-b42-source-pdf-${{ github.run_id }} + path: ${{ runner.temp }}/i61-evidence/ + if-no-files-found: warn diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a21636..1594761d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased — I61 independent real B42 acceptance (candidate; NOT_RUN) + +- Re-lease existing #92 stacked QA onto B42 production HEAD `91e2d8201cd0` with native C six-gesture X11 driver; no changes to A/B/C/E/F production. Exact app-tree provenance remains fail-closed. +- Replace broken doubled-escape `pdffonts` parser with all-font embedding audit; four offline positive/negative tests run before installing external XeLaTeX. Actual GUI source/Save/Open/Undo/help/PDF is NOT_RUN until new exact-head artifact acceptance. Physical Win10/Mac ARM NOT_RUN. + + ## Unreleased — B42 redesigned Studio and dedicated user's MiKTeX Windows Server QA (PENDING) - Follow-up from real B42 X11 screenshot review: restore a dark Midnight default and readable 14pt source/body fonts, relocate grouped palette away from source, search the live C registry instead of hard-coded packages, and keep manual PDF zoom stable when selecting PDF. Exact-head reacceptance PENDING. diff --git a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md new file mode 100644 index 00000000..c4b1fe71 --- /dev/null +++ b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md @@ -0,0 +1,30 @@ +# I61 B42 independent actual Studio acceptance — 2026-11-02 + +Main `93db2044b5b91f79c0e774be8806a3693ba7329b`; B42 `dadd05b16c0c917f28a57f233b12931fbbcb98cd` (snapshot). The previous I51/I57 source-tree profiles still target B41 and fail **before launching egui** on B42, so neither genuine PASS nor actual functionality FAIL can be inferred. The B42 bootstrap proves a real window and opening the *Geometry group popup*, **not** a selected command or PDF. + +This QA commit adds a **separate full Studio X11 acceptance candidate**, source/PDF SHA ledger, all-six tool icon and pointer sequence and external XeLaTeX PDF predicates; no production code changes. It pins B42's exact app/src tree and captures screenshots and files on early failures for subsequent coordinate repair. Strictly no offscreen Session call masquerades as a real GUI interaction. + +**Evidence at authoring:** code ready for hosted action; execution, artifact checks and manual screenshot interpretation **NOT_RUN**. Even a green automated run needs manual visual review and does not cover all Undo/Open/CRLF gates or physical OS acceptance. Do not merge this QA or B42 on this statement alone. + +## First I61 actual-X11 run — failed, root cause and repair + +Exact previous QA HEAD `0ca4f3c8a69b67c026b5ed142af2b4242e266e07`, real [PR workflow 41425235038](https://github.com/i-shutov/tikzarium/actions/runs/41425235038), original artifact **13131797362** independently downloaded and inspected. Strict exact app-tree lease PASSED; 24.04 hosted TeXLive/X11 prerequisites and locked production Rust build PASSED; actual Studio process started and produced screenshot `01-real-studio.png`. `02-file-menu.png` was **byte-identical** to the first image (SHA-256 `faada4760c84d33c85af0a15b8cac0ea3192bcb001a400a672e3b9bf7d95ccc2`) instead of showing the menu. Driver then blocked for 25 seconds in `xclip`, never reached a real GUI Save or TikZ pointer. Report correctly FAIL / remaining NOT_RUN. **No product source defect established.** + +Independent analysis identified two QA-driver defects: X11 commands used `xdotool mousemove --window` with coordinates measured from the **root screenshot**, thereby adding WM titlebar y offset; and `run()` captured pipes of the intentionally backgrounded xclip owner, keeping `subprocess.run` open. Fix uses root pixel coordinates for click/gesture and noncapturing xclip consistent with previous I47 desktop QA. Also fixes a tautological source-hash comparison at PDF export. New exact-head run and manual visual acceptance remain NOT_RUN until CI completes. + +## Second I61 run — real source input proved, File path click recalibrated + +Exact QA HEAD `5a46488d6d201d91cc1879ae71f74580fed932bd` [run 41425938749](https://github.com/i-shutov/tikzarium/actions/runs/41425938749), original artifact **13131254633** independently downloaded and screenshots inspected. The previous client/root mismatch and xclip timeout are resolved: the **real Studio File menu opens** (screenshot `02-file-menu.png` differs from startup; field is visibly at x307..575 y83..103), and original user TeX with Russian `Не менять` and opaque `\\UserLabel` is **visibly pasted into the live editable source pane** (`04-source-pasted.png`). However `03-path-in-file-menu.png` equals the closed startup image: click at y101 hit the input's lower border and dismissed the menu; with no valid path, real Studio Save refused to create a file. This is a QA coordinate error, not proof of a product Save defect. No native tools/PDF touched, report correctly FAIL and NOT_RUN. Correct input click to center (420,93) on the next QA HEAD, add strict all-font per-row embedding predicate to the later PDF stage. Execution/artifact review for corrected HEAD NOT_RUN at commit. + +## Third actual-X11 artifact review — custom File-path edit remains unresolved + +Exact head `5f4c6c59e6dec776551aea32a32360fd5f8ddb97`, [run 41426644654](https://github.com/i-shutov/tikzarium/actions/runs/41426644654), original artifact **13132690797** downloaded. Real menu and manually pasted Unicode/opaque source are visually present, but candidate popup path click still dismissed the menu before input; GUI Save did not write a file. This is NOT a demonstrated product Save defect. Further coordinate nudges would obscure the main native-authoring vertical. + +Revised independent driver starts the actual egui process with **isolated cwd=out**, relies on actual GUI File-menu default `figure.tex` shown in screenshot, pastes original .tex via X11 and uses actual toolbar Save to create the file. Then six grouped-icon TikZ gestures and external XeLaTeX PDF can be evaluated without fabricating input/output or touching product code. Custom-path menu editing explicitly stays NOT_RUN. Duplicate pull_request action trigger removed to avoid exact-commit run cancellation by the subsequent push event; QA branch push only. New exact-head CI NOT_RUN at commit. + + +### Integration re-lease to frozen B42 (2026-11-02 UTC) + +Production HEAD #91 is `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`; independent I61 QA remains a stacked PR targeting the B branch. Latest B42 bootstrap [41445054318](https://github.com/i-shutov/tikzarium/actions/runs/41445054318) reports two PNG hashes `4367ce5d798befaf2a3a157089db23df4cc5540b2d83814d94bacb15bb9f5766` and `df7cdc32f345f4988bad8de5029ababd396fb5f720f80d464945cb90f06ee1df`, identical to already manually reviewed B42 layout. Thus button coordinates are **candidate only**, not a gesture PASS. New font parser checks ALL `pdffonts` embedding flags from the right with four positive/negative offline tests, not an exit-code proxy. + +I61 still requires actual original GUI saved-source, six pointer gestures, help, XeLaTeX PDF and independent artifact SHA/manual review. Older I51/I57 B41-tree FAILs occur before GUI and are stale gates; no smoke or synthetic event-frame is a substitute. Physical Win10 MiKTeX/Mac ARM remain NOT_RUN. diff --git a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md new file mode 100644 index 00000000..ec8aad66 --- /dev/null +++ b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md @@ -0,0 +1,30 @@ +# I61 · independent B42 real-egui Studio source/PDF acceptance + +Baseline B42 PR #91 HEAD `dadd05b16c0c917f28a57f233b12931fbbcb98cd`; app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`. Independent D/integrator QA **stacked on B42**, not production B code, main, or qa/i06-n1-desktop-pointer. + +## User-visible vertical + +Real Linux/X11 `tikzarium-app` launches with isolated XDG folders and the selected external TeX Live XeLaTeX. It clicks **File → path TextEdit**, pastes the original Unicode/opaque-macro source into the actual editable source pane, presses the **GUI Save icon**, clicks grouped TikZ icons and six individual native C tools, sends six physical XTest pointer drags/clicks, and demands one additional real GUI-persisted command after each gesture. Every step asserts the original source's bytes outside validated authored C commands. The grouped popups, selected tool, pointer, saved source, F1 open/close and PDF viewport are screenshoted separately. GUI Build → PDF tab → Export must yield an external XeLaTeX one-page PDF with Cyrillic extracted and DejaVu embedded; Poppler PNG is retained. + +The tested 1366×768 control coordinates come from independently reviewed B42 bootstrap screenshot SHA `b5f717856087c397b9a87a98965f581a180e6f055256724072a240f413538cad` and group SHA `d3be3595ba89bed353a8af1a7c11a81c173ac4140722c2e1edc213e92a1e3c77`. **File-menu, individual tool and Undo coordinates are provisional until output screenshots are independently checked**, and failures must not be retried by silently accepting another UI element. + +## Remaining gates and evidence classifications + +- New exact-head GitHub Actions, actual screenshot/source/PDF and independent SHA review are **NOT_RUN at authoring**, not PASS. +- This initial full six-tool path does **not** assert two-file dirty Open, CRLF on-disk loaded-file preservation, inspector pinned Apply, visual Undo or native Ctrl+Z in the actual B42 window; report marks each NOT_RUN. They are still required for B42 merge acceptance using further independent tests, not removed requirements. +- Hosted real X11 is not physical Win10/MiKTeX or macOS ARM M1/M4. Both are **NOT_RUN**. Hosted Server MiKTeX is not physical Win10; the user Windows Server self-hosted runner remains unverified. +- No engine substitutions, TeX parser/CAS, package installer, second TikZ catalogue, source regeneration or B/C/A/F production changes. +- QA branch targets B42, never merge into main independently. Require full exact-head CI, manual screenshot/PDF/SHA review, and then exact-head merge into B42 and separate B42-to-main checks. + +First artifact review: prior QA run 41425235038 failed *inside QA driver* before native gestures. Fix X11 root/client coordinate origin and background xclip pipe inheritance in next QA HEAD. Do not mark a B42 production failure; new exact-head real process CI and screenshot calibration required. + +Second inspected artifact 13131254633: real File menu and source Ctrl+V visually PASS, path TextEdit hit at y101 failed and Save properly refused; reaccept on calibrated y93. PDF audit strengthened to require embedding for ALL records, not a single DejaVu yes. Actual six tools and exact PDF remain NOT_RUN. + +Third reviewed run 41426644654: custom-path popup TextEdit failed again despite visible real menu; no product Save defect proved. Use isolated app cwd and the GUI's visible default figure.tex, via actual toolbar Save. Custom path TextEdit NOT_RUN; new exact-head source/PDF evidence pending. + + +### Integration re-lease to frozen B42 (2026-11-02 UTC) + +Production HEAD #91 is `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`; independent I61 QA remains a stacked PR targeting the B branch. Latest B42 bootstrap [41445054318](https://github.com/i-shutov/tikzarium/actions/runs/41445054318) reports two PNG hashes `4367ce5d798befaf2a3a157089db23df4cc5540b2d83814d94bacb15bb9f5766` and `df7cdc32f345f4988bad8de5029ababd396fb5f720f80d464945cb90f06ee1df`, identical to already manually reviewed B42 layout. Thus button coordinates are **candidate only**, not a gesture PASS. New font parser checks ALL `pdffonts` embedding flags from the right with four positive/negative offline tests, not an exit-code proxy. + +I61 still requires actual original GUI saved-source, six pointer gestures, help, XeLaTeX PDF and independent artifact SHA/manual review. Older I51/I57 B41-tree FAILs occur before GUI and are stale gates; no smoke or synthetic event-frame is a substitute. Physical Win10 MiKTeX/Mac ARM remain NOT_RUN. diff --git a/scripts/qa_i61_b42_studio_x11.py b/scripts/qa_i61_b42_studio_x11.py new file mode 100644 index 00000000..3facc6dd --- /dev/null +++ b/scripts/qa_i61_b42_studio_x11.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +"""I61: real B42 Studio X11 group-click -> six native TikZ gestures -> saved TeX/PDF. + +Never infer PASS from a screenshot alone; this script requires actual process +events and bytes that the running GUI persisted. Independent manual inspection +of screenshots/PDF remains mandatory. Linux/Xvfb != physical Win10 or Mac ARM. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import subprocess +import time + +from qa_n1_desktop import FIXTURE, original_preserved, owners + +# Candidate click coordinates are tied to the exact app/src Git tree and +# previously inspected B42 bootstrap screenshots, not a general GUI contract. +APP_TREE = "64df229392c03a99ca29238bcc2e5f422c4d7409" +BUTTONS = [ + ("line", (30, 150), (599, 169), (646, 350), (750, 377)), + ("rectangle", (30, 150), (653, 169), (681, 425), (765, 477)), + ("circle", (30, 150), (707, 169), (810, 351), (840, 351)), + ("cubic", (30, 150), (761, 169), (777, 470), (902, 460)), + ("arrow", (30, 199), (599, 216), (615, 535), (743, 552)), + ("node", (30, 248), (599, 266), (850, 550), (851, 550)), +] + + +def sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + + +def audit_embedded_fonts(output: str) -> list[str]: + """Require all PDF fonts embedded; parse the rightmost pdffonts columns. + + Type may have spaces (CID TrueType). The five final columns are + emb, sub, uni, object, ID. One nonembedded font fails the audit. + """ + rows = [line for line in output.splitlines()[2:] if line.strip()] + if not rows: + raise RuntimeError("No font records in pdffonts") + for line in rows: + columns = line.split() + flags = columns[-5:] + if ( + len(columns) < 8 or len(flags) != 5 + or any(flag.lower() not in ("yes", "no") for flag in flags[:3]) + or not flags[3].isdigit() or not flags[4].isdigit() + ): + raise RuntimeError("Unparseable PDF font row: " + line) + if flags[0].lower() != "yes": + raise RuntimeError("Nonembedded PDF font: " + line) + return rows + + +def run(*argv: str, timeout: int = 25) -> str: + result = subprocess.run(argv, capture_output=True, timeout=timeout, check=False) + if result.returncode: + raise RuntimeError(f"{argv[0]} exit {result.returncode}: " + + result.stderr.decode("utf-8", "replace")[-1400:]) + return result.stdout.decode("utf-8", "replace").strip() + + +def wait(predicate, detail: str, seconds: float = 12.0) -> None: + until = time.monotonic() + seconds + while time.monotonic() < until: + if predicate(): + return + time.sleep(0.20) + raise RuntimeError(detail) + + +def paste(contents: bytes, path: Path) -> None: + path.write_bytes(contents) + # xclip backgrounds an X11 clipboard owner. Capturing its stdout/stderr + # via run() keeps inherited pipes open, causing a false 25s timeout. + # Do not capture the detached owner; match independently accepted I47 QA. + subprocess.run(["xclip", "-selection", "clipboard", "-i", str(path)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=8, check=True) + run("xdotool", "key", "--clearmodifiers", "ctrl+v") + time.sleep(0.28) + + +def screenshot(out: Path, label: str) -> None: + filename = out / (label + ".png") + run("import", "-window", "root", str(filename), timeout=35) + if filename.stat().st_size < 5000: + raise RuntimeError(f"Empty X11 screenshot: {label}") + + +def click(window: str, x: int, y: int) -> None: + # Reviewed screenshots show ROOT pixels. xdotool --window instead uses + # client-relative coordinates and double-counts the 20px WM titlebar. + run("xdotool", "mousemove", "--sync", str(x), str(y), "click", "1") + time.sleep(0.20) + + +def key(value: str) -> None: + run("xdotool", "key", "--clearmodifiers", value) + time.sleep(0.18) + + +def gesture(window: str, start: tuple[int, int], end: tuple[int, int]) -> None: + run("xdotool", "mousemove", "--sync", str(start[0]), str(start[1]), + "mousedown", "1") + time.sleep(0.15) + run("xdotool", "mousemove", "--sync", str(end[0]), str(end[1])) + time.sleep(0.15) + run("xdotool", "mouseup", "1") + time.sleep(0.35) + + +def main() -> int: + arg = argparse.ArgumentParser(description=__doc__) + arg.add_argument("--app", required=True, type=Path) + arg.add_argument("--out", required=True, type=Path) + args = arg.parse_args() + out = args.out.resolve() + if out.exists() and any(out.iterdir()): + arg.error("I61 requires an empty evidence directory") + out.mkdir(parents=True, exist_ok=True) + if os.environ.get("TIKZARIUM_APP_TREE_SHA") != APP_TREE: + arg.error("B42 app/source tree changed: re-review screenshot coordinates first") + # The real app's default file field is "figure.tex" (reviewed screenshot). + # Use an isolated GUI process working directory instead of an unproved + # popup path TextEdit click. The app must write this .tex via its Save icon. + source = out / "figure.tex" + fixture = FIXTURE.encode("utf-8") + report = { + "schema_version": 1, "kind": "I61_B42_REAL_STUDIO_GESTURES", + "status": "FAIL", "gui": "LINUX_X11_HOSTED", + "app_tree_sha": APP_TREE, + "source_head_sha": os.environ.get("TIKZARIUM_SOURCE_SHA", "UNKNOWN"), + "host": platform.platform(), "physical_win10_miktex": "NOT_RUN", + "physical_mac_m1_m4": "NOT_RUN", + "manual_image_review": "NOT_RUN", "gui_save": "NOT_RUN", + "six_real_tools": "NOT_RUN", "real_xelatex_pdf": "NOT_RUN", + "gui_open_reopen": "NOT_RUN", "visual_undo": "NOT_RUN", + "native_text_undo": "NOT_RUN", "library_inspector": "NOT_RUN", + "steps": [], + } + process = None + app_log = (out / "app.log").open("wb") + try: + env = os.environ.copy() + env.update({ + "XDG_CONFIG_HOME": str(out / "config"), + "XDG_DATA_HOME": str(out / "data"), + "XDG_CACHE_HOME": str(out / "cache"), + "LIBGL_ALWAYS_SOFTWARE": "1", + "WINIT_UNIX_BACKEND": "x11", + }) + env.pop("WAYLAND_DISPLAY", None) + process = subprocess.Popen([str(args.app.resolve(strict=True))], + cwd=out, env=env, stdout=app_log, stderr=app_log) + found = None + until = time.monotonic() + 45 + while time.monotonic() < until: + if process.poll() is not None: + raise RuntimeError(f"Studio process exited {process.returncode}") + match = subprocess.run( + ["xdotool", "search", "--onlyvisible", "--pid", str(process.pid), + "--name", "TikZarium"], + capture_output=True, timeout=5, check=False) + if match.returncode == 0 and match.stdout.strip(): + found = match.stdout.decode().splitlines()[-1] + break + time.sleep(0.25) + if not found: + raise RuntimeError("No real visible Studio process window") + run("xdotool", "windowmove", found, "0", "0") + run("xdotool", "windowsize", "--sync", found, "1366", "768") + run("xdotool", "windowactivate", "--sync", found) + time.sleep(1.6) + report["window_id"] = found + report["app_sha256"] = sha(args.app) + screenshot(out, "01-real-studio") + + # Real File-popup screenshot confirms "figure.tex" as app default. + # GUI starts in out/, so clicking real Save writes out/figure.tex. + # Popup's editable custom path is separately NOT_RUN. + click(found, 321, 40) + screenshot(out, "02-file-menu-default-path") + click(found, 130, 209) + key("ctrl+a") + paste(fixture, out / "clipboard-source.txt") + screenshot(out, "04-source-pasted") + click(found, 177, 39) + wait(source.is_file, "Studio toolbar Save did not create default figure.tex in isolated cwd") + if source.read_bytes() != fixture: + raise RuntimeError("Real GUI Save altered opaque Unicode source") + report["file_menu_path_edit"] = "NOT_RUN_DEFAULT_FIGURE_TEX_USED" + report["gui_save"] = "PASS_REAL_X11_EXACT_BYTES" + report["steps"].append({"case": "paste_and_save", "sha256": sha(source)}) + screenshot(out, "05-source-saved") + + for index, (tool, group, tool_button, start, end) in enumerate(BUTTONS, 1): + before = source.read_bytes() + count = len(owners(before)) + click(found, *group) + screenshot(out, f"{index:02d}-{tool}-group-popup") + click(found, *tool_button) + screenshot(out, f"{index:02d}-{tool}-selected") + gesture(found, start, end) + screenshot(out, f"{index:02d}-{tool}-pointer") + # Save is the actual Studio toolbar button, no synthetic file edits. + click(found, 177, 39) + time.sleep(0.5) + after = source.read_bytes() + authored = owners(after) + if len(authored) != count + 1 or authored[-1]["tool"] != tool: + raise RuntimeError( + f"{tool}: real group/tool/pointer did not persist a new " + f"native owned command (before={count}, after={authored})") + if not original_preserved(fixture, after): + raise RuntimeError(f"{tool}: source bytes outside C command changed") + if tool == "cubic" and b".. controls " not in after: + raise RuntimeError("Cubic did not produce native TikZ Bezier") + report["steps"].append({ + "case": "real_group_and_pointer_" + tool, + "source_sha256": sha(source), + "owner_count": len(authored), + "owner_id": authored[-1]["id"], + }) + screenshot(out, f"{index:02d}-{tool}-saved") + + report["six_real_tools"] = "PASS_REAL_X11_SAVED_SOURCE" + key("F1") + screenshot(out, "20-f1-help") + key("F1") + screenshot(out, "21-f1-closed") + report["f1_keystrokes"] = "SENT_REAL_X11_VISUAL_REVIEW_REQUIRED" + report["final_source_sha256"] = sha(source) + click(found, 219, 39) # Real Studio Build icon, external XeLaTeX. + pdf = source.with_suffix(".pdf") + # The app may retain the build PDF in memory until Export; Build success + # alone must not be interpreted as a persisted generated file. + time.sleep(3.0) + screenshot(out, "22-build-status") + click(found, 73, 77) # Studio PDF tab, code pane still visible. + time.sleep(1.5) + screenshot(out, "23-pdf-view-before-export") + click(found, 259, 39) # Real export action. + wait(pdf.is_file, "Studio Export did not write a real XeLaTeX PDF", 20) + if sha(source) != report["final_source_sha256"]: + raise RuntimeError("Saved source changed during PDF export") + metadata = run("pdfinfo", str(pdf)) + if "Pages: 1" not in metadata: + raise RuntimeError("Expected one-page exact XeLaTeX PDF") + run("pdftoppm", "-f", "1", "-l", "1", "-singlefile", "-png", + "-r", "120", str(pdf), str(out / "24-exact-pdf")) + text = run("pdftotext", "-enc", "UTF-8", str(pdf), "-") + if "Не менять" not in text: + raise RuntimeError("Cyrillic opaque macro absent from real PDF") + fonts = run("pdffonts", str(pdf)) + rows = audit_embedded_fonts(fonts) + (out / "pdffonts.txt").write_text(fonts + "\n", encoding="utf-8") + (out / "pdfinfo.txt").write_text(metadata + "\n", encoding="utf-8") + if not any("DejaVu" in line for line in rows): + raise RuntimeError("Expected DejaVu embedded font absent") + report["real_xelatex_pdf"] = "PASS_EXTERNAL_XELATEX_PDF_CYRILLIC_AND_ALL_FONTS_EMBEDDED" + report["pdf_sha256"] = sha(pdf) + screenshot(out, "25-pdf-exported") + report["status"] = "PASS_AUTOMATED_REAL_X11_REQUIRES_MANUAL_ARTIFACT_REVIEW" + except Exception as exc: + report["error"] = f"{type(exc).__name__}: {exc}" + finally: + if process: + process.terminate() + try: + process.wait(timeout=8) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + app_log.close() + report["files"] = { + file.name: sha(file) for file in sorted(out.iterdir()) + if file.is_file() and file.name != "report.json" + } + (out / "report.json").write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + print(json.dumps(report, ensure_ascii=False)) + return 0 if report["status"].startswith("PASS_AUTOMATED") else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tests/test_qa_i61_studio_fonts.py b/scripts/tests/test_qa_i61_studio_fonts.py new file mode 100644 index 00000000..ed1122b7 --- /dev/null +++ b/scripts/tests/test_qa_i61_studio_fonts.py @@ -0,0 +1,33 @@ +"""I61 offline PDF font proof contract tests, not actual GUI evidence.""" +from pathlib import Path +import sys +import unittest +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from qa_i61_b42_studio_x11 import audit_embedded_fonts + +HEADER = ("name type encoding emb sub uni object ID\n" + "-------------------------------\n") + +class AuditFonts(unittest.TestCase): + def test_all_embedded_including_two_word_type(self): + lines = audit_embedded_fonts( + HEADER + "DejaVuSerif TrueType WinAnsi yes yes yes 6 0\n" + + "LatinModernMath CID TrueType Identity-H yes yes yes 12 0\n") + self.assertEqual(len(lines), 2) + + def test_single_nonembedded_font_fails(self): + with self.assertRaisesRegex(RuntimeError, "Nonembedded"): + audit_embedded_fonts( + HEADER + "DejaVuSerif TrueType WinAnsi yes yes yes 6 0\n" + + "OtherFont TrueType WinAnsi no no yes 8 0\n") + + def test_malformed_row_fails(self): + with self.assertRaisesRegex(RuntimeError, "Unparseable"): + audit_embedded_fonts(HEADER + "BadFont not enough fields\n") + + def test_empty_inventory_fails(self): + with self.assertRaisesRegex(RuntimeError, "No font records"): + audit_embedded_fonts(HEADER) + +if __name__ == "__main__": + unittest.main() From f3aa1ea7c9459cf27443d908ab78866576eccd01 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 17:07:25 +0300 Subject: [PATCH 44/52] test(I61): replace legacy B41 automatic X11 gates with exact B42 source/PDF regression --- .github/workflows/i46-b34-dirty-open-x11.yml | 4 ++-- .github/workflows/i53-b38-live-libraries.yml | 3 ++- .github/workflows/i57-n2-four-live-x11.yml | 6 +++-- .github/workflows/i61-b42-studio-x11.yml | 22 ++++++++++++++++++- .github/workflows/n1-desktop-smoke.yml | 4 ++-- CHANGELOG.md | 4 +++- .../2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md | 9 ++++++++ ...1-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md | 9 ++++++++ 8 files changed, 52 insertions(+), 9 deletions(-) diff --git a/.github/workflows/i46-b34-dirty-open-x11.yml b/.github/workflows/i46-b34-dirty-open-x11.yml index 9b31131c..8d996b24 100644 --- a/.github/workflows/i46-b34-dirty-open-x11.yml +++ b/.github/workflows/i46-b34-dirty-open-x11.yml @@ -1,3 +1,5 @@ +# Legacy B41 coordinate profile; the current B42 Studio uses I61 real-X11 source/PDF QA. +# Historical B41 branch/explicit dispatch remain available with exact B41 provenance. name: I51 B37 real X11 dirty-Open preserve user TeX and PDF on: push: @@ -10,7 +12,6 @@ on: - qa/i54-b39-n2-n3-desktop-reaccept - qa/i60-b41-inspector-real-x11 - b41-egui-pinned-parameter-apply - - main paths: - '.github/workflows/i46-b34-dirty-open-x11.yml' - 'scripts/qa_i46_dirty_open_x11.py' @@ -30,7 +31,6 @@ on: - b40-egui-source-undo-transaction-fence - b/b32-safe-open-source-undo-20261101 - b/b36-compact-pdf-focus - - main paths: - '.github/workflows/i46-b34-dirty-open-x11.yml' - 'scripts/qa_i46_dirty_open_x11.py' diff --git a/.github/workflows/i53-b38-live-libraries.yml b/.github/workflows/i53-b38-live-libraries.yml index 62cbbbc9..52d55f02 100644 --- a/.github/workflows/i53-b38-live-libraries.yml +++ b/.github/workflows/i53-b38-live-libraries.yml @@ -1,3 +1,5 @@ +# Legacy B41 coordinate profile; the current B42 Studio uses I61 real-X11 source/PDF QA. +# Historical B41 branch/explicit dispatch remain available with exact B41 provenance. name: I53 B39 actual X11 library Connect Disconnect PDF Undo and GUI Reopen on: push: @@ -8,7 +10,6 @@ on: - feat/b38-n2-n3-inspector-20261101 - qa/i60-b41-inspector-real-x11 - b41-egui-pinned-parameter-apply - - main workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/i57-n2-four-live-x11.yml b/.github/workflows/i57-n2-four-live-x11.yml index f3e5a332..0d69ef3b 100644 --- a/.github/workflows/i57-n2-four-live-x11.yml +++ b/.github/workflows/i57-n2-four-live-x11.yml @@ -1,7 +1,9 @@ +# Legacy B41 coordinate profile; the current B42 Studio uses I61 real-X11 source/PDF QA. +# Historical B41 branch/explicit dispatch remain available with exact B41 provenance. name: I57 N2 four actual X11 library source PDF Undo Open and C help on: pull_request: - branches: [main, b40-egui-source-undo-transaction-fence, b41-egui-pinned-parameter-apply] + branches: [b40-egui-source-undo-transaction-fence, b41-egui-pinned-parameter-apply] paths: - '.github/workflows/i57-n2-four-live-x11.yml' - 'scripts/qa_i57_four_libs_x11.py' @@ -12,7 +14,7 @@ on: - 'crates/**' - 'Cargo.lock' push: - branches: [main, qa/i58-b40-source-history-x11, qa/i60-b41-inspector-real-x11, b41-egui-pinned-parameter-apply] + branches: [qa/i58-b40-source-history-x11, qa/i60-b41-inspector-real-x11, b41-egui-pinned-parameter-apply] paths: - '.github/workflows/i57-n2-four-live-x11.yml' - 'scripts/qa_i57_four_libs_x11.py' diff --git a/.github/workflows/i61-b42-studio-x11.yml b/.github/workflows/i61-b42-studio-x11.yml index ab1145cd..2857ca45 100644 --- a/.github/workflows/i61-b42-studio-x11.yml +++ b/.github/workflows/i61-b42-studio-x11.yml @@ -1,7 +1,27 @@ +# B42 six native GUI gestures, persisted source, external XeLaTeX PDF. +# Exact app-tree profile is fail-closed; Open/Undo/Apply/F1 and physical OS not covered. name: I61 B42 real Studio icon-to-native-source/PDF (independent D QA) on: push: - branches: [qa/i61-b42-studio-source-pdf] + branches: [qa/i61-b42-studio-source-pdf, main] + paths: + - '.github/workflows/i61-b42-studio-x11.yml' + - 'scripts/qa_i61_b42_studio_x11.py' + - 'scripts/qa_n1_desktop.py' + - 'scripts/tests/test_qa_i61_studio_fonts.py' + - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' + pull_request: + branches: [main] + paths: + - '.github/workflows/i61-b42-studio-x11.yml' + - 'scripts/qa_i61_b42_studio_x11.py' + - 'scripts/qa_n1_desktop.py' + - 'scripts/tests/test_qa_i61_studio_fonts.py' + - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/n1-desktop-smoke.yml b/.github/workflows/n1-desktop-smoke.yml index 0ece0cc6..a5413825 100644 --- a/.github/workflows/n1-desktop-smoke.yml +++ b/.github/workflows/n1-desktop-smoke.yml @@ -1,3 +1,5 @@ +# Legacy B41 coordinate profile; the current B42 Studio uses I61 real-X11 source/PDF QA. +# Historical B41 branch/explicit dispatch remain available with exact B41 provenance. name: I51 B37 real X11 native TikZ six gestures, held Undo and PDF on: push: @@ -12,7 +14,6 @@ on: - qa/i54-b39-n2-n3-desktop-reaccept - qa/i60-b41-inspector-real-x11 - b41-egui-pinned-parameter-apply - - main paths: - '.github/workflows/n1-desktop-smoke.yml' - 'scripts/qa_n1_desktop.py' @@ -35,7 +36,6 @@ on: - b/b32-safe-open-source-undo-20261101 - feat/b27-egui-undo-held-pointer-20261101 - b/b31-compact-xelatex-fit - - main paths: - '.github/workflows/n1-desktop-smoke.yml' - 'scripts/qa_n1_desktop.py' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1594761d..e6022896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## Unreleased — I61 independent real B42 acceptance (candidate; NOT_RUN) +## Unreleased — I61 independent real B42 acceptance (six-gesture subvertical PASS; N1 NOT_ACCEPTED) + +- CI transition: historical B41 I51/I53/I57 fixed-tree X11 workflows remain manually runnable at matching B41 revision, no automatic false-red main runs; I61 exact-B42 six-gesture/source/PDF regression now runs for main PRs and pushes. Real B42 Open/Undo/Apply/F1 and physical targets are still unaccepted release gates. - Re-lease existing #92 stacked QA onto B42 production HEAD `91e2d8201cd0` with native C six-gesture X11 driver; no changes to A/B/C/E/F production. Exact app-tree provenance remains fail-closed. - Replace broken doubled-escape `pdffonts` parser with all-font embedding audit; four offline positive/negative tests run before installing external XeLaTeX. Actual GUI source/Save/Open/Undo/help/PDF is NOT_RUN until new exact-head artifact acceptance. Physical Win10/Mac ARM NOT_RUN. diff --git a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md index c4b1fe71..b9a47bc0 100644 --- a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md +++ b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md @@ -28,3 +28,12 @@ Revised independent driver starts the actual egui process with **isolated cwd=ou Production HEAD #91 is `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`; independent I61 QA remains a stacked PR targeting the B branch. Latest B42 bootstrap [41445054318](https://github.com/i-shutov/tikzarium/actions/runs/41445054318) reports two PNG hashes `4367ce5d798befaf2a3a157089db23df4cc5540b2d83814d94bacb15bb9f5766` and `df7cdc32f345f4988bad8de5029ababd396fb5f720f80d464945cb90f06ee1df`, identical to already manually reviewed B42 layout. Thus button coordinates are **candidate only**, not a gesture PASS. New font parser checks ALL `pdffonts` embedding flags from the right with four positive/negative offline tests, not an exit-code proxy. I61 still requires actual original GUI saved-source, six pointer gestures, help, XeLaTeX PDF and independent artifact SHA/manual review. Older I51/I57 B41-tree FAILs occur before GUI and are stale gates; no smoke or synthetic event-frame is a substitute. Physical Win10 MiKTeX/Mac ARM remain NOT_RUN. + + +## CI ownership migration — 2026-11-02 + +B42 is frozen at `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`. Current independent real-X11 six-tool → GUI saved native-source → external XeLaTeX PDF evidence: [I61 run 41449047973](https://github.com/i-shutov/tikzarium/actions/runs/41449047973), original artifact 13143756910, 40/40 SHA-256 rechecked. I61 now provides B42 six-tool regression on PR/main and push/main; if a future app/src tree changes, the exact source-tree and coordinate lease must be re-reviewed rather than silently passing stale pointer positions. + +Four historical B41 fixed-tree GUI workflows (`n1-desktop-smoke` I51, `i46-b34-dirty-open` I51, `i53-b38-live-libraries` I53, and `i57-n2-four-live` I57) no longer run automatically against `main`. They retain explicit workflow_dispatch and their historical branch triggers, including the original B41 tree check: **actual tests were not removed**. Their B42 red runs failed on hardcoded tree `d290bdad...` **before any GUI**, not on a real B42 interaction. I61 does **not** substitute for their Open/Undo/library assertions. + +**Still blocked, NOT_RUN at B42 real GUI level:** two-file Open/dirty Save-and-open with CRLF and opaque macro, source/native and visual Undo after manual changes, inspector pinned Apply, deterministic Help/F1 (I61 frames 20/21 ambiguous), connected libraries; physical Win10/MiKTeX and Mac ARM XeLaTeX. These are required release gates, not waived by a green six-gesture smoke. Merge #92 into frozen #91, then #91 into main only after full independent B42 evidence and exact-head CI with reviewed original artifacts; run separate post-merge CI. diff --git a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md index ec8aad66..08d10a3f 100644 --- a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md +++ b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md @@ -28,3 +28,12 @@ Third reviewed run 41426644654: custom-path popup TextEdit failed again despite Production HEAD #91 is `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`; independent I61 QA remains a stacked PR targeting the B branch. Latest B42 bootstrap [41445054318](https://github.com/i-shutov/tikzarium/actions/runs/41445054318) reports two PNG hashes `4367ce5d798befaf2a3a157089db23df4cc5540b2d83814d94bacb15bb9f5766` and `df7cdc32f345f4988bad8de5029ababd396fb5f720f80d464945cb90f06ee1df`, identical to already manually reviewed B42 layout. Thus button coordinates are **candidate only**, not a gesture PASS. New font parser checks ALL `pdffonts` embedding flags from the right with four positive/negative offline tests, not an exit-code proxy. I61 still requires actual original GUI saved-source, six pointer gestures, help, XeLaTeX PDF and independent artifact SHA/manual review. Older I51/I57 B41-tree FAILs occur before GUI and are stale gates; no smoke or synthetic event-frame is a substitute. Physical Win10 MiKTeX/Mac ARM remain NOT_RUN. + + +## CI ownership migration — 2026-11-02 + +B42 is frozen at `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df229392c03a99ca29238bcc2e5f422c4d7409`. Current independent real-X11 six-tool → GUI saved native-source → external XeLaTeX PDF evidence: [I61 run 41449047973](https://github.com/i-shutov/tikzarium/actions/runs/41449047973), original artifact 13143756910, 40/40 SHA-256 rechecked. I61 now provides B42 six-tool regression on PR/main and push/main; if a future app/src tree changes, the exact source-tree and coordinate lease must be re-reviewed rather than silently passing stale pointer positions. + +Four historical B41 fixed-tree GUI workflows (`n1-desktop-smoke` I51, `i46-b34-dirty-open` I51, `i53-b38-live-libraries` I53, and `i57-n2-four-live` I57) no longer run automatically against `main`. They retain explicit workflow_dispatch and their historical branch triggers, including the original B41 tree check: **actual tests were not removed**. Their B42 red runs failed on hardcoded tree `d290bdad...` **before any GUI**, not on a real B42 interaction. I61 does **not** substitute for their Open/Undo/library assertions. + +**Still blocked, NOT_RUN at B42 real GUI level:** two-file Open/dirty Save-and-open with CRLF and opaque macro, source/native and visual Undo after manual changes, inspector pinned Apply, deterministic Help/F1 (I61 frames 20/21 ambiguous), connected libraries; physical Win10/MiKTeX and Mac ARM XeLaTeX. These are required release gates, not waived by a green six-gesture smoke. Merge #92 into frozen #91, then #91 into main only after full independent B42 evidence and exact-head CI with reviewed original artifacts; run separate post-merge CI. From ca05d8bbc752bd258e1454de883400c230a72669 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 18:17:12 +0300 Subject: [PATCH 45/52] test(I62): exercise real F1, native/visual Undo, CRLF Open/Reopen and dirty Cancel; bound apt mirror stalls --- .github/workflows/i61-b42-studio-x11.yml | 9 +- CHANGELOG.md | 5 + .../2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md | 7 + ...1-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md | 7 + scripts/qa_i61_b42_studio_x11.py | 155 ++++++++++++++++++ 5 files changed, 181 insertions(+), 2 deletions(-) diff --git a/.github/workflows/i61-b42-studio-x11.yml b/.github/workflows/i61-b42-studio-x11.yml index 2857ca45..3dbddd82 100644 --- a/.github/workflows/i61-b42-studio-x11.yml +++ b/.github/workflows/i61-b42-studio-x11.yml @@ -51,8 +51,13 @@ jobs: run: python3 -m unittest discover -s scripts/tests -p test_qa_i61_studio_fonts.py -v - name: Hosted Linux/X11 and external TeX Live XeLaTeX prerequisites run: | - sudo apt-get update -q - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + # Exact f3aa1ea run cancelled after 45min downloading 129MB apt packages. + # Avoid the observed severely slow Azure archive and bound HTTP stalls. + if test -f /etc/apt/apt-mirrors.txt; then + sudo sed -i 's#http://azure.archive.ubuntu.com/ubuntu/#http://archive.ubuntu.com/ubuntu/#g' /etc/apt/apt-mirrors.txt + fi + sudo apt-get -o Acquire::Retries=2 -o Acquire::http::Timeout=25 update -q + sudo DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=2 -o Acquire::http::Timeout=25 install -y --no-install-recommends \ xvfb xauth openbox xdotool xclip imagemagick libgl1-mesa-dri libglx-mesa0 \ libxkbcommon-dev libxkbcommon-x11-dev libx11-dev libxi-dev \ libxcursor-dev libxrandr-dev libudev-dev \ diff --git a/CHANGELOG.md b/CHANGELOG.md index e6022896..bfda7c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased — I62 real N1 GUI Help, Undo and two-file I/O acceptance (QA ONLY) + +- Extend existing stacked #92 independent Linux/X11 driver to test actual F1 open/close, TextEdit Ctrl+Z, visual Undo, second CRLF/opaque-source GUI Open/Save/Reopen and dirty Open cancel; separate NOT_RUN until exact-head CI and manual artifacts. No B/C/A production edits. +- Diagnose exact I61 QA head f3aa1ea7 cancellation before GUI (45-minute Azure apt mirror stall); use canonical Ubuntu archive and bounded package retries/HTTP timeouts, never replace external XeLaTeX or waive evidence. + ## Unreleased — I61 independent real B42 acceptance (six-gesture subvertical PASS; N1 NOT_ACCEPTED) - CI transition: historical B41 I51/I53/I57 fixed-tree X11 workflows remain manually runnable at matching B41 revision, no automatic false-red main runs; I61 exact-B42 six-gesture/source/PDF regression now runs for main PRs and pushes. Real B42 Open/Undo/Apply/F1 and physical targets are still unaccepted release gates. diff --git a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md index b9a47bc0..196e21ac 100644 --- a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md +++ b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md @@ -37,3 +37,10 @@ B42 is frozen at `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df2 Four historical B41 fixed-tree GUI workflows (`n1-desktop-smoke` I51, `i46-b34-dirty-open` I51, `i53-b38-live-libraries` I53, and `i57-n2-four-live` I57) no longer run automatically against `main`. They retain explicit workflow_dispatch and their historical branch triggers, including the original B41 tree check: **actual tests were not removed**. Their B42 red runs failed on hardcoded tree `d290bdad...` **before any GUI**, not on a real B42 interaction. I61 does **not** substitute for their Open/Undo/library assertions. **Still blocked, NOT_RUN at B42 real GUI level:** two-file Open/dirty Save-and-open with CRLF and opaque macro, source/native and visual Undo after manual changes, inspector pinned Apply, deterministic Help/F1 (I61 frames 20/21 ambiguous), connected libraries; physical Win10/MiKTeX and Mac ARM XeLaTeX. These are required release gates, not waived by a green six-gesture smoke. Merge #92 into frozen #91, then #91 into main only after full independent B42 evidence and exact-head CI with reviewed original artifacts; run separate post-merge CI. + + +## I62 correction: exact-head CI cancellation and real GUI I/O acceptance expansion + +The new QA head f3aa1ea7 run 41458975300 job 125322826120 completed with GitHub `cancelled` during apt package downloads, not a Rust/TeX/product test FAIL. GitHub log: 76 packages/129MB via azure.archive.ubuntu.com, texlive-base 21.7MB from 14:18 to 14:36 UTC and texlive-pictures 16.7MB from 14:40 until cancellation at 14:53 UTC. Exact app-tree check and offline font-parser tests passed, but app compilation, real gestures and PDF were SKIPPED. The previously accepted cc339308 I61 run remains valid for its exact head only. New apt URL preference and HTTP inactivity/retry settings aim to avoid repeated 45-minute waste. + +New QA-only probes target true Linux/X11 F1 two-state tab, native source undo, session visual Undo, two-file CRLF Open/Edit/Save/Reopen and dirty Open cancellation. All are NOT_RUN pending exact-head CI and original evidence review; any QA focus/coordinate miss must be investigated, not promoted to product FAIL or synthetic PASS. No B production/registry changes. Inspector Apply, dirty Save-and-open and physical targets stay NOT_RUN. diff --git a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md index 08d10a3f..36fca0dd 100644 --- a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md +++ b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md @@ -37,3 +37,10 @@ B42 is frozen at `91e2d8201cd0c016e00d52618e4024fedb66424e`, app/src tree `64df2 Four historical B41 fixed-tree GUI workflows (`n1-desktop-smoke` I51, `i46-b34-dirty-open` I51, `i53-b38-live-libraries` I53, and `i57-n2-four-live` I57) no longer run automatically against `main`. They retain explicit workflow_dispatch and their historical branch triggers, including the original B41 tree check: **actual tests were not removed**. Their B42 red runs failed on hardcoded tree `d290bdad...` **before any GUI**, not on a real B42 interaction. I61 does **not** substitute for their Open/Undo/library assertions. **Still blocked, NOT_RUN at B42 real GUI level:** two-file Open/dirty Save-and-open with CRLF and opaque macro, source/native and visual Undo after manual changes, inspector pinned Apply, deterministic Help/F1 (I61 frames 20/21 ambiguous), connected libraries; physical Win10/MiKTeX and Mac ARM XeLaTeX. These are required release gates, not waived by a green six-gesture smoke. Merge #92 into frozen #91, then #91 into main only after full independent B42 evidence and exact-head CI with reviewed original artifacts; run separate post-merge CI. + + +## I62 additional real GUI gates + +QA head f3aa1ea7 run 41458975300 was CANCELLED at 45min while acquiring 129MB Ubuntu/TeX Live packages on the Azure apt mirror, after passing app-source SHA and four PDF font-audit tests. This is CI infrastructure, not an executed GUI regression; no new source/PDF proof on this head. Switch hosted CI to Ubuntu archive mirror with bounded HTTP timeouts, while retaining external TeX Live XeLaTeX identity. Previous accepted I61 real X11 six gestures/PDF (cc339308, run 41449047973, 40/40 SHA) remain separate evidence. + +Extend the *same* I61 real-egui QA script after verified six primitive GUI Save/PDF with actual F1 Help tab two-state images, TextEdit Ctrl+Z, seventh native gesture and visual Undo, two-file CRLF/opaque macro GUI Open/Edit/Save, Ctrl+O Reopen after explicit external fixture modification, dirty Open -> Escape Cancel -> Save, and original SHA/image artifacts on failure. These new probes are NOT_RUN until exact-head CI and independent manual image/source/PDF inspection; unexpected focus/coordinate actions must remain FAIL. Inspector pinned Apply, dirty Save-and-open, project libraries, physical Win10 and Mac ARM remain NOT_RUN pending real tests. diff --git a/scripts/qa_i61_b42_studio_x11.py b/scripts/qa_i61_b42_studio_x11.py index 3facc6dd..d0a75800 100644 --- a/scripts/qa_i61_b42_studio_x11.py +++ b/scripts/qa_i61_b42_studio_x11.py @@ -117,6 +117,21 @@ def gesture(window: str, start: tuple[int, int], end: tuple[int, int]) -> None: time.sleep(0.35) + +def held_f1() -> None: + # An instantaneous key tap may miss egui's next input frame. + run("xdotool", "keydown", "F1") + time.sleep(0.34) + run("xdotool", "keyup", "F1") + time.sleep(0.48) + + +def dock_signature(out: Path, label: str) -> str: + # Compare the right-hand actual Studio tab bar, not its changing canvas. + return run("convert", str(out / (label + ".png")), "-crop", + "330x42+1025+94", "+repage", "-format", "%#", "info:") + + def main() -> int: arg = argparse.ArgumentParser(description=__doc__) arg.add_argument("--app", required=True, type=Path) @@ -144,6 +159,8 @@ def main() -> int: "six_real_tools": "NOT_RUN", "real_xelatex_pdf": "NOT_RUN", "gui_open_reopen": "NOT_RUN", "visual_undo": "NOT_RUN", "native_text_undo": "NOT_RUN", "library_inspector": "NOT_RUN", + "f1_two_state": "NOT_RUN", "gui_two_file_crlf": "NOT_RUN", + "dirty_open_cancel": "NOT_RUN", "inspector_apply": "NOT_RUN", "steps": [], } process = None @@ -268,6 +285,144 @@ def main() -> int: report["real_xelatex_pdf"] = "PASS_EXTERNAL_XELATEX_PDF_CYRILLIC_AND_ALL_FONTS_EMBEDDED" report["pdf_sha256"] = sha(pdf) screenshot(out, "25-pdf-exported") + + # I62: real desktop assertions follow the already-proven six gestures + # and PDF. Source bytes and screenshots must be independently reviewed. + click(found, 28, 77) # Return to the guide from the PDF viewport. + click(found, 1055, 109) # Real Properties tab. + click(found, 402, 39) # Help toolbar ON. + click(found, 402, 39) # Help toolbar OFF; deterministic initial state. + screenshot(out, "26-properties-before-f1") + properties = dock_signature(out, "26-properties-before-f1") + held_f1() + screenshot(out, "27-f1-help-open") + opened = dock_signature(out, "27-f1-help-open") + held_f1() + screenshot(out, "28-f1-help-closed") + closed = dock_signature(out, "28-f1-help-closed") + if opened == properties or closed != properties: + raise RuntimeError("Real F1 failed Properties -> Help -> Properties tab transition") + report["f1_two_state"] = "PASS_REAL_X11_TAB_CHANGE_MANUAL_LABEL_REVIEW_REQUIRED" + + # TextEdit Ctrl+Z and Session visual inverse-patch Undo are different. + six_bytes = source.read_bytes() + manual = b"% I62_NATIVE_UNDO_MARKER" + click(found, 180, 380) + key("ctrl+End") + paste(manual, out / "clipboard-manual-undo.txt") + screenshot(out, "29-manual-source-edited") + click(found, 177, 39) + wait(lambda: manual in source.read_bytes(), "Real manual GUI edit was not persisted") + if source.read_bytes() != six_bytes + manual: + raise RuntimeError("Manual edit rewrote unrelated source bytes") + click(found, 180, 380) + key("ctrl+End") + key("ctrl+z") + screenshot(out, "30-native-ctrl-z") + click(found, 177, 39) + wait(lambda: source.read_bytes() == six_bytes, + "Native Ctrl+Z failed to restore exactly six-tool source") + report["native_text_undo"] = "PASS_REAL_X11_SAVED_SOURCE_BYTES" + + click(found, 30, 150) # Geometry group. + click(found, 599, 169) # Native shared-C line. + gesture(found, (845, 400), (948, 421)) + click(found, 177, 39) + wait(lambda: len(owners(source.read_bytes())) == 7, + "Seventh actual GUI gesture did not persist a native command") + screenshot(out, "31-seven-before-visual-undo") + click(found, 1085, 157) # Visible inspector visual Undo. + screenshot(out, "32-visual-undo-click") + click(found, 177, 39) + wait(lambda: source.read_bytes() == six_bytes, + "Visual inverse-patch Undo failed to restore six-tool source") + report["visual_undo"] = "PASS_REAL_X11_INVERSE_SOURCE_BYTES" + + # The second file is an explicit test INPUT, never forged GUI output. + secondary = out / "second-crlf.tex" + secondary_input = ( + "\\documentclass{standalone}\r\n" + "\\usepackage{tikz}\r\n" + "% I62 preserve original CRLF comment\r\n" + "\\newcommand{\\OpaqueSecond}{Не менять}\r\n" + "\\begin{document}\r\n" + "\\begin{tikzpicture}\r\n" + "\\node at (0,0) {\\OpaqueSecond};\r\n" + "\\end{tikzpicture}\r\n" + "\\end{document}\r\n" + ).encode("utf-8") + secondary.write_bytes(secondary_input) + click(found, 321, 40) + time.sleep(0.45) + screenshot(out, "33-file-menu-before-open") + click(found, 440, 93) # Actual B42 File-menu TextEdit center. + key("ctrl+a") + paste(b"second-crlf.tex", out / "clipboard-open-path.txt") + screenshot(out, "34-second-path-entered") + click(found, 351, 124) # Actual GUI 'Открыть путь'. + time.sleep(0.75) + screenshot(out, "35-second-opened") + append = b"% I62_GUI_SECOND_SAVED" + click(found, 177, 380) + key("ctrl+End") + paste(append, out / "clipboard-second-edit.txt") + click(found, 177, 39) + wait(lambda: append in secondary.read_bytes(), + "Real second-file GUI Open/Edit/Save did not persist") + if secondary.read_bytes() != secondary_input + append: + raise RuntimeError("Second-file GUI Save normalized CRLF or opaque bytes") + if source.read_bytes() != six_bytes: + raise RuntimeError("Second-file Save corrupted first document") + screenshot(out, "36-second-crlf-saved") + report["gui_two_file_crlf"] = "PASS_REAL_X11_TWO_FILE_GUI_OPEN_SAVE" + + # Explicit external test-fixture modification proves Ctrl+O reads disk. + external = b"% I62_EXTERNAL_BEFORE_REOPEN" + secondary.write_bytes(secondary_input + append + external) + key("ctrl+o") + time.sleep(0.65) + screenshot(out, "37-second-reopened") + after_reopen = b"% I62_AFTER_GUI_REOPEN" + click(found, 177, 380) + key("ctrl+End") + paste(after_reopen, out / "clipboard-reopen-edit.txt") + click(found, 177, 39) + wait(lambda: after_reopen in secondary.read_bytes(), + "Reopened GUI document cannot be edited and saved") + if secondary.read_bytes() != secondary_input + append + external + after_reopen: + raise RuntimeError("GUI Ctrl+O failed to reload external CRLF source") + report["gui_open_reopen"] = "PASS_REAL_X11_BYTE_EXACT_RELOAD" + + # Real dirty Open -> Cancel: never discard the original unsaved edit. + pending = b"% I62_DIRTY_CANCEL_SURVIVES" + click(found, 177, 380) + key("ctrl+End") + paste(pending, out / "clipboard-dirty-source.txt") + screenshot(out, "38-dirty-source") + click(found, 321, 40) + time.sleep(0.44) + click(found, 440, 93) + key("ctrl+a") + paste(b"figure.tex", out / "clipboard-first-path.txt") + click(found, 351, 124) + time.sleep(0.55) + screenshot(out, "39-pending-dirty-open") + key("Escape") + time.sleep(0.40) + screenshot(out, "40-dirty-open-cancelled") + click(found, 177, 39) + wait(lambda: pending in secondary.read_bytes(), + "Dirty Open Cancel discarded unsaved source") + if secondary.read_bytes() != secondary_input + append + external + after_reopen + pending: + raise RuntimeError("Dirty Open cancel corrupted second source") + if source.read_bytes() != six_bytes: + raise RuntimeError("Dirty Open changed original first document") + report["dirty_open_cancel"] = "PASS_REAL_X11_CANCEL_AND_SAVE_BYTES" + report["steps"].append({ + "case": "f1_native_visual_undo_two_file_crlf_reopen_dirty_cancel", + "first_source_sha256": sha(source), "second_source_sha256": sha(secondary) + }) + report["status"] = "PASS_AUTOMATED_REAL_X11_REQUIRES_MANUAL_ARTIFACT_REVIEW" except Exception as exc: report["error"] = f"{type(exc).__name__}: {exc}" From 10c6f17a101750d314acd22597bae62e16d737df Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 18:18:26 +0300 Subject: [PATCH 46/52] fix(I62 QA): escape TeX fixture commands while retaining real CRLF bytes --- scripts/qa_i61_b42_studio_x11.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/qa_i61_b42_studio_x11.py b/scripts/qa_i61_b42_studio_x11.py index d0a75800..c70b4e01 100644 --- a/scripts/qa_i61_b42_studio_x11.py +++ b/scripts/qa_i61_b42_studio_x11.py @@ -341,15 +341,15 @@ def main() -> int: # The second file is an explicit test INPUT, never forged GUI output. secondary = out / "second-crlf.tex" secondary_input = ( - "\\documentclass{standalone}\r\n" - "\\usepackage{tikz}\r\n" + "\\\documentclass{standalone}\r\n" + "\\\usepackage{tikz}\r\n" "% I62 preserve original CRLF comment\r\n" - "\\newcommand{\\OpaqueSecond}{Не менять}\r\n" - "\\begin{document}\r\n" - "\\begin{tikzpicture}\r\n" - "\\node at (0,0) {\\OpaqueSecond};\r\n" - "\\end{tikzpicture}\r\n" - "\\end{document}\r\n" + "\\\newcommand{\\\OpaqueSecond}{Не менять}\r\n" + "\\\begin{document}\r\n" + "\\\begin{tikzpicture}\r\n" + "\\\node at (0,0) {\\\OpaqueSecond};\r\n" + "\\\end{tikzpicture}\r\n" + "\\\end{document}\r\n" ).encode("utf-8") secondary.write_bytes(secondary_input) click(found, 321, 40) From e4f68187ccdd944b67151ac18955d1540b2b176d Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 18:18:49 +0300 Subject: [PATCH 47/52] fix(I62 QA): restore exactly doubled literal TeX escapes with real CRLF fixture --- scripts/qa_i61_b42_studio_x11.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/qa_i61_b42_studio_x11.py b/scripts/qa_i61_b42_studio_x11.py index c70b4e01..d0a75800 100644 --- a/scripts/qa_i61_b42_studio_x11.py +++ b/scripts/qa_i61_b42_studio_x11.py @@ -341,15 +341,15 @@ def main() -> int: # The second file is an explicit test INPUT, never forged GUI output. secondary = out / "second-crlf.tex" secondary_input = ( - "\\\documentclass{standalone}\r\n" - "\\\usepackage{tikz}\r\n" + "\\documentclass{standalone}\r\n" + "\\usepackage{tikz}\r\n" "% I62 preserve original CRLF comment\r\n" - "\\\newcommand{\\\OpaqueSecond}{Не менять}\r\n" - "\\\begin{document}\r\n" - "\\\begin{tikzpicture}\r\n" - "\\\node at (0,0) {\\\OpaqueSecond};\r\n" - "\\\end{tikzpicture}\r\n" - "\\\end{document}\r\n" + "\\newcommand{\\OpaqueSecond}{Не менять}\r\n" + "\\begin{document}\r\n" + "\\begin{tikzpicture}\r\n" + "\\node at (0,0) {\\OpaqueSecond};\r\n" + "\\end{tikzpicture}\r\n" + "\\end{document}\r\n" ).encode("utf-8") secondary.write_bytes(secondary_input) click(found, 321, 40) From a6e1da2a28e2d5b31793d5a93180a026bc518282 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 21:25:58 +0300 Subject: [PATCH 48/52] fix(B42): retain File path focus in egui popup with event-frame regression --- crates/tikzarium-app/src/main.rs | 234 ++++++++++++++++-- insights/2026-11-02_B42_UX_EVIDENCE.md | 6 + ...2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md | 8 + 3 files changed, 228 insertions(+), 20 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 9fa92165..2aa10883 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -507,6 +507,83 @@ struct TikzariumApp { font_proof_texture: Option, } +// B42: the path TextEdit must not close its menu just by gaining focus. +// egui 0.33 defaults to CloseOnClick, including clicks inside the menu. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum StudioFileAction { + OpenPath, + SaveAs, +} + +struct StudioFileMenuResponse { + action: Option, + #[cfg(test)] + trigger_rect: egui::Rect, + #[cfg(test)] + path_rect: Option, + #[cfg(test)] + open_rect: Option, +} + +fn show_studio_file_menu( + ui: &mut egui::Ui, + file_field: &mut String, + ready: bool, +) -> StudioFileMenuResponse { + let mut result = StudioFileMenuResponse { + action: None, + #[cfg(test)] + trigger_rect: egui::Rect::NOTHING, + #[cfg(test)] + path_rect: None, + #[cfg(test)] + open_rect: None, + }; + let (trigger, _) = egui::containers::menu::MenuButton::new("Файл") + .config( + egui::containers::menu::MenuConfig::default() + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside), + ) + .ui(ui, |ui| { + ui.label("TeX-проект:"); + let path_edit = ui.add( + egui::TextEdit::singleline(file_field) + .desired_width(260.0) + .hint_text("figure.tex"), + ); + #[cfg(test)] + { + result.path_rect = Some(path_edit.rect); + } + #[cfg(not(test))] + let _ = path_edit; + + let open = ui.add_enabled(ready, egui::Button::new("Открыть путь")); + #[cfg(test)] + { + result.open_rect = Some(open.rect); + } + if open.clicked() { + result.action = Some(StudioFileAction::OpenPath); + ui.close(); + } + if ui + .add_enabled(ready, egui::Button::new("Сохранить как…")) + .clicked() + { + result.action = Some(StudioFileAction::SaveAs); + ui.close(); + } + }); + #[cfg(test)] + { + result.trigger_rect = trigger.rect; + } + #[cfg(not(test))] + let _ = trigger; + result +} + impl TikzariumApp { fn new(cc: &eframe::CreationContext<'_>) -> Self { let prefs = cc @@ -1193,28 +1270,13 @@ impl TikzariumApp { cancel.cancel(); } ui.separator(); - ui.menu_button("Файл", |ui| { - ui.label("TeX-проект:"); - ui.add( - egui::TextEdit::singleline(&mut self.file_field) - .desired_width(260.0) - .hint_text("figure.tex"), - ); - if ui - .add_enabled(ready, egui::Button::new("Открыть путь")) - .clicked() - { + match show_studio_file_menu(ui, &mut self.file_field, ready).action { + Some(StudioFileAction::OpenPath) => { self.open_path(PathBuf::from(self.file_field.trim())); - ui.close(); } - if ui - .add_enabled(ready, egui::Button::new("Сохранить как…")) - .clicked() - { - self.save_as(); - ui.close(); - } - }); + Some(StudioFileAction::SaveAs) => self.save_as(), + None => {} + } ui.menu_button("Вид", |ui| { if ui .checkbox(&mut self.prefs.studio_tool_rail, "Группы инструментов") @@ -3815,3 +3877,135 @@ mod split_layout_tests { assert!(!authoring.undo_available()); } } + +#[cfg(test)] +mod b42_file_popup_tests { + use super::*; + + fn frame( + ctx: &egui::Context, + file_field: &mut String, + events: Vec, + modifiers: egui::Modifiers, + ) -> StudioFileMenuResponse { + let mut result = None; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(850.0, 560.0), + )), + events, + modifiers, + ..Default::default() + }, + |ctx| { + egui::TopBottomPanel::top("b42-file-popup-event").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.strong("TikZarium"); + ui.separator(); + result = Some(show_studio_file_menu(ui, file_field, true)); + }); + }); + egui::CentralPanel::default().show(ctx, |_ui| {}); + }, + ); + result.expect("the production File menu was not laid out") + } + + fn pointer(pos: egui::Pos2, pressed: bool) -> egui::Event { + egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed, + modifiers: egui::Modifiers::default(), + } + } + + fn click( + ctx: &egui::Context, + file_field: &mut String, + pos: egui::Pos2, + ) -> (StudioFileMenuResponse, StudioFileMenuResponse) { + frame( + ctx, + file_field, + vec![egui::Event::PointerMoved(pos), pointer(pos, true)], + Default::default(), + ); + let released = frame(ctx, file_field, vec![pointer(pos, false)], Default::default()); + let settled = frame(ctx, file_field, vec![], Default::default()); + (released, settled) + } + + #[test] + fn b42_file_textedit_click_keeps_popup_open_then_real_open_action() { + // I62 repro: old ui.menu_button closed the popup upon focusing TextEdit. + // UNIT_EVENT through the actual production menu, NOT physical X11. + let directory = tempfile::tempdir().unwrap(); + let second = directory.path().join("second-crlf.tex"); + let untouched = "% Не менять\r\n\\OpaqueSecond{тест}\r\n"; + std::fs::write(&second, untouched).unwrap(); + + let ctx = egui::Context::default(); + let mut field = "figure.tex".to_owned(); + let initial = frame(&ctx, &mut field, vec![], Default::default()); + assert!(initial.trigger_rect.is_positive()); + let (_, opened) = click(&ctx, &mut field, initial.trigger_rect.center()); + assert!(opened.path_rect.is_some(), "File popup did not open"); + + let (_, focused) = click(&ctx, &mut field, opened.path_rect.unwrap().center()); + assert!( + focused.path_rect.is_some(), + "clicking inside File TextEdit must not dismiss the popup" + ); + let control = egui::Modifiers { + ctrl: true, + command: true, + ..Default::default() + }; + frame( + &ctx, + &mut field, + vec![egui::Event::Key { + key: egui::Key::A, + physical_key: None, + pressed: true, + repeat: false, + modifiers: control, + }], + control, + ); + frame( + &ctx, + &mut field, + vec![egui::Event::Key { + key: egui::Key::A, + physical_key: None, + pressed: false, + repeat: false, + modifiers: control, + }], + Default::default(), + ); + let entered = frame( + &ctx, + &mut field, + vec![egui::Event::Text("second-crlf.tex".into())], + Default::default(), + ); + assert_eq!( + field, "second-crlf.tex", + "focused File TextEdit must replace the path" + ); + assert!(entered.path_rect.is_some(), "typing must not close the menu"); + + let visible = frame(&ctx, &mut field, vec![], Default::default()); + let open_rect = visible.open_rect.expect("Open button not visible after editing"); + let (activated, closed) = click(&ctx, &mut field, open_rect.center()); + assert_eq!(activated.action, Some(StudioFileAction::OpenPath)); + assert!(closed.path_rect.is_none(), "Open must explicitly close File"); + assert_eq!(std::fs::read(&second).unwrap(), untouched.as_bytes()); + assert_eq!(Document::load(&second).unwrap().source, untouched); + } +} diff --git a/insights/2026-11-02_B42_UX_EVIDENCE.md b/insights/2026-11-02_B42_UX_EVIDENCE.md index 1b7dd5ea..d145b264 100644 --- a/insights/2026-11-02_B42_UX_EVIDENCE.md +++ b/insights/2026-11-02_B42_UX_EVIDENCE.md @@ -189,3 +189,9 @@ and no false visual Undo. These tests do not mean true desktop GUI acceptance. D #92 Studio X11 six gestures, manual source Ctrl+Z, two-file Save/Open and exact XeLaTeX PDF remain gates; physical Windows10/M1/M4 and the owner's Windows Server remain NOT_RUN. + +## B42 P0 File-popup forensic correction and fix (2026-11-02) + +I62 run 41470355844 artifact 13154818704: root coordinate (440,93) IS inside File path TextEdit (screenshot 33). Screenshot 34 shows collapsed menu before path entry, screenshot 35 is byte-identical. Prior incorrect-coordinate hypothesis was retracted. egui 0.33.3 PopupCloseBehavior default is CloseOnClick, including clicks inside menu: production ui.menu_button File was unsuitable for an embedded TextEdit. Source-level repro is confirmed; a physical corrected executable has NOT_RUN. + +Fix only the B-owned File menu to CloseOnClickOutside and keep explicit closing for activated Open/Save As. The new event-frame test of the real extracted handler asserts that focusing does not dismiss the popup, Ctrl+A and typing edit the path, Open closes explicitly, and a separate CRLF/Russian/opaque fixture is untouched. Against the old default click-to-close semantics the first keep-open assertion would fail; no measured pre-fix red-run is claimed. No changes to catalogues, compiler, themes or D QA. New exact-head CI and independent I62 full X11 acceptance are required before merge; prior six primitive → source → XeLaTeX PDF proof remains historical evidence. diff --git a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md index 34b7f2ab..bf29a28d 100644 --- a/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md +++ b/plans/2026-11-02_B42_STUDIO_UX_WINDOWS_SERVER.md @@ -183,3 +183,11 @@ A/D/E/F source, TeX/CAS/solver or PDF-to-coordinates. Classification: UNIT_EVENT only, not physical Windows 10/M1/M4 or full Linux/X11. #91 remains draft pending exact-head hosted fmt/test/Clippy and independent D #92 real six-tool source-to-XeLaTeX acceptance. + +## P0 File popup click-to-edit repair — 2026-11-02 + +D I62 real Linux/X11 run 41470355844 showed a correct click into the File menu TextEdit, immediate popup collapse and failed second-file Open. Exact pinned egui 0.33.3 default closes a menu on ANY click, even inside its controls. No test-coordinate shift is appropriate. This is a verified production design blocker. + +In existing B #91 alone, File uses MenuButton + MenuConfig CloseOnClickOutside. Actual Open and Save As still call ui.close explicitly and retain safe file/dirty-confirm logic. A new headless egui Context::run(RawInput) regression uses the exact extracted production File handler to click TextEdit, replace path, click Open and check an independent CRLF/Cyrillic/opaque fixture. No C/A/D/E/F production code, separate PR, custom TeX, or workflow changes. UNIT_EVENT does NOT establish an actual successful X11 two-file Open. + +Hold merge until exact new-head Rust/Clippy/D QA passes and D #92 re-leases the new app/source tree for the real Open/Edit/Save/Reopen, dirty Cancel, inspector Apply, source/PDF/fonts and manual PNG audit. Physical Windows 10/MiKTeX and Mac ARM external XeLaTeX remain NOT_RUN. From f1e16af5f093f162ac6611ebc3219f8c59dba272 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 21:27:33 +0300 Subject: [PATCH 49/52] style(B42): apply exact rustfmt output for File popup event regression --- crates/tikzarium-app/src/main.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index 2aa10883..d3f3b333 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -3933,7 +3933,12 @@ mod b42_file_popup_tests { vec![egui::Event::PointerMoved(pos), pointer(pos, true)], Default::default(), ); - let released = frame(ctx, file_field, vec![pointer(pos, false)], Default::default()); + let released = frame( + ctx, + file_field, + vec![pointer(pos, false)], + Default::default(), + ); let settled = frame(ctx, file_field, vec![], Default::default()); (released, settled) } @@ -3998,13 +4003,21 @@ mod b42_file_popup_tests { field, "second-crlf.tex", "focused File TextEdit must replace the path" ); - assert!(entered.path_rect.is_some(), "typing must not close the menu"); + assert!( + entered.path_rect.is_some(), + "typing must not close the menu" + ); let visible = frame(&ctx, &mut field, vec![], Default::default()); - let open_rect = visible.open_rect.expect("Open button not visible after editing"); + let open_rect = visible + .open_rect + .expect("Open button not visible after editing"); let (activated, closed) = click(&ctx, &mut field, open_rect.center()); assert_eq!(activated.action, Some(StudioFileAction::OpenPath)); - assert!(closed.path_rect.is_none(), "Open must explicitly close File"); + assert!( + closed.path_rect.is_none(), + "Open must explicitly close File" + ); assert_eq!(std::fs::read(&second).unwrap(), untouched.as_bytes()); assert_eq!(Document::load(&second).unwrap().source, untouched); } From 09a4df5baf0f7446bb02c68fbb126821d2a0ff9f Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 21:29:57 +0300 Subject: [PATCH 50/52] fix(B42): preserve unit match branches for Save As action --- crates/tikzarium-app/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tikzarium-app/src/main.rs b/crates/tikzarium-app/src/main.rs index d3f3b333..c31de61e 100644 --- a/crates/tikzarium-app/src/main.rs +++ b/crates/tikzarium-app/src/main.rs @@ -1274,7 +1274,9 @@ impl TikzariumApp { Some(StudioFileAction::OpenPath) => { self.open_path(PathBuf::from(self.file_field.trim())); } - Some(StudioFileAction::SaveAs) => self.save_as(), + Some(StudioFileAction::SaveAs) => { + self.save_as(); + } None => {} } ui.menu_button("Вид", |ui| { From 74b67c1b77a7071b2ea4ca6eae4e321e6a28f5d2 Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 22:00:21 +0300 Subject: [PATCH 51/52] qa(I63): enforce both B42 tree leases and preserve File Open focus evidence --- .github/workflows/i61-b42-studio-x11.yml | 2 ++ CHANGELOG.md | 4 ++++ insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md | 5 +++++ plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md | 5 +++++ scripts/qa_i61_b42_studio_x11.py | 7 +++++-- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/i61-b42-studio-x11.yml b/.github/workflows/i61-b42-studio-x11.yml index cd7f5b92..94ca9737 100644 --- a/.github/workflows/i61-b42-studio-x11.yml +++ b/.github/workflows/i61-b42-studio-x11.yml @@ -44,6 +44,8 @@ jobs: - name: Exact B42 app source-tree lease (fail closed before GUI) run: | test "$(git rev-parse HEAD:crates/tikzarium-app/src)" = '72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb' + # Catch disagreement between workflow and Python QA before heavy TeX install. + grep -Fqx "APP_TREE = \"$(git rev-parse HEAD:crates/tikzarium-app/src)\"" scripts/qa_i61_b42_studio_x11.py echo "TIKZARIUM_APP_TREE_SHA=$(git rev-parse HEAD:crates/tikzarium-app/src)" >> "$GITHUB_ENV" echo "TIKZARIUM_SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" git diff --exit-code -- Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 63dc02e4..5bceda37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased — I63 dual B42 source-tree lease & real File Open focus snapshots + +- Pin workflow + script to current B42 app/source Git tree `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`; reject mismatches before TeX Live installation. Capture real focus/Ctrl+A/paste screenshots while retaining all six native gestures, source/Undo/Open/PDF assertions. NOT_RUN full GUI and physical hosts until exact-head artifact audit. + ## Unreleased — I63 B42 P0 File Open integration lease (actual X11 NOT_RUN at commit) - Merge frozen B42 File TextEdit click-to-edit production repair into existing stacked #92 as a second-parent exact SHA, preserving all prior D QA changes; move I61 app-source-tree guard to `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`. Do not disable source-tree provenance or replace external TeX Live with MiKTeX in Linux/X11 QA. diff --git a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md index 24948869..5685cc90 100644 --- a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md +++ b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md @@ -51,3 +51,8 @@ New QA-only probes target true Linux/X11 F1 two-state tab, native source undo, s Independent integrator re-leased I61 on frozen B42 #91 `09a4df5baf0f7446bb02c68fbb126821d2a0ff9f`, verified `crates/tikzarium-app/src` Git tree `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb` by GitHub Git tree API. Old B head `91e2d820…` and app tree `64df229392c03a99ca29238bcc2e5f422c4d7409` are historical; the QA workflow now pins the new tree and rejects any further B production drift. B only changed its File popup behavior (egui 0.33.3 CloseOnClickOutside) plus B plans/insights, preserving the C generator and all six tools. Exact B head hosted Rust CI, multi-OS D QA, generated-source XeLaTeX, hosted MiKTeX, font audits all passed; B test is `UNIT_EVENT`, not a successful real two-file X11 Open. One stacked integration commit combines B's three-file change with the existing #92 QA-only workflow re-lease, this plan/insights and CHANGELOG. Do not rerun old B41 hard-coded SHA guards and do not create new QA micro-PRs. New D exact-head real Linux/X11 must prove File TextEdit focus→input→real Open of independent CRLF/opaque second file, edit/save/reopen, dirty Cancel and preserve six native gestures, F1/Undo, Unicode embedded-font XeLaTeX PDF. Inspector pinned Apply remains a separate explicit real GUI release gate unless this execution proves it. Manual source/PDF/screenshot/hash audit mandatory before either merge. Physical Win10/MiKTeX, M1/M4 XeLaTeX NOT_RUN. + + +## I63 QA double-lease correction — 2026-11-02 + +The integrator caught an additional historical B app-tree SHA hardcoded in the Python real-X11 driver (not only the workflow), which would reject the correct B42 executable before GUI. Both now bind exact `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`; lightweight CI preflight asserts equality BEFORE installing TeX Live. Added screenshots immediately after File TextEdit pointer focus and Ctrl+A to distinguish focus from clipboard paste if two-file Open fails. Same stacked QA #92 only, no B edits/new PR. Prior first I63 head is superseded on this exact issue; any CI failure from that old head is stale QA rather than demonstrated product regression. Complete Open/Undo/Apply and physical Win10/M1/M4 remain unaccepted until evidence. diff --git a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md index b2623d51..b965d7c6 100644 --- a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md +++ b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md @@ -51,3 +51,8 @@ Extend the *same* I61 real-egui QA script after verified six primitive GUI Save/ Independent integrator re-leased I61 on frozen B42 #91 `09a4df5baf0f7446bb02c68fbb126821d2a0ff9f`, verified `crates/tikzarium-app/src` Git tree `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb` by GitHub Git tree API. Old B head `91e2d820…` and app tree `64df229392c03a99ca29238bcc2e5f422c4d7409` are historical; the QA workflow now pins the new tree and rejects any further B production drift. B only changed its File popup behavior (egui 0.33.3 CloseOnClickOutside) plus B plans/insights, preserving the C generator and all six tools. Exact B head hosted Rust CI, multi-OS D QA, generated-source XeLaTeX, hosted MiKTeX, font audits all passed; B test is `UNIT_EVENT`, not a successful real two-file X11 Open. One stacked integration commit combines B's three-file change with the existing #92 QA-only workflow re-lease, this plan/insights and CHANGELOG. Do not rerun old B41 hard-coded SHA guards and do not create new QA micro-PRs. New D exact-head real Linux/X11 must prove File TextEdit focus→input→real Open of independent CRLF/opaque second file, edit/save/reopen, dirty Cancel and preserve six native gestures, F1/Undo, Unicode embedded-font XeLaTeX PDF. Inspector pinned Apply remains a separate explicit real GUI release gate unless this execution proves it. Manual source/PDF/screenshot/hash audit mandatory before either merge. Physical Win10/MiKTeX, M1/M4 XeLaTeX NOT_RUN. + + +## I63 QA double-lease correction — 2026-11-02 + +The integrator caught an additional historical B app-tree SHA hardcoded in the Python real-X11 driver (not only the workflow), which would reject the correct B42 executable before GUI. Both now bind exact `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`; lightweight CI preflight asserts equality BEFORE installing TeX Live. Added screenshots immediately after File TextEdit pointer focus and Ctrl+A to distinguish focus from clipboard paste if two-file Open fails. Same stacked QA #92 only, no B edits/new PR. Prior first I63 head is superseded on this exact issue; any CI failure from that old head is stale QA rather than demonstrated product regression. Complete Open/Undo/Apply and physical Win10/M1/M4 remain unaccepted until evidence. diff --git a/scripts/qa_i61_b42_studio_x11.py b/scripts/qa_i61_b42_studio_x11.py index d0a75800..98dff963 100644 --- a/scripts/qa_i61_b42_studio_x11.py +++ b/scripts/qa_i61_b42_studio_x11.py @@ -20,7 +20,7 @@ # Candidate click coordinates are tied to the exact app/src Git tree and # previously inspected B42 bootstrap screenshots, not a general GUI contract. -APP_TREE = "64df229392c03a99ca29238bcc2e5f422c4d7409" +APP_TREE = "72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb" BUTTONS = [ ("line", (30, 150), (599, 169), (646, 350), (750, 377)), ("rectangle", (30, 150), (653, 169), (681, 425), (765, 477)), @@ -355,8 +355,10 @@ def main() -> int: click(found, 321, 40) time.sleep(0.45) screenshot(out, "33-file-menu-before-open") - click(found, 440, 93) # Actual B42 File-menu TextEdit center. + click(found, 440, 93) # Exact root-pixel point inside File TextEdit. + screenshot(out, "33a-file-path-focused") key("ctrl+a") + screenshot(out, "33b-file-path-selected") paste(b"second-crlf.tex", out / "clipboard-open-path.txt") screenshot(out, "34-second-path-entered") click(found, 351, 124) # Actual GUI 'Открыть путь'. @@ -375,6 +377,7 @@ def main() -> int: raise RuntimeError("Second-file Save corrupted first document") screenshot(out, "36-second-crlf-saved") report["gui_two_file_crlf"] = "PASS_REAL_X11_TWO_FILE_GUI_OPEN_SAVE" + report["file_menu_path_edit"] = "PASS_REAL_X11_EDITABLE_FILE_PATH" # Explicit external test-fixture modification proves Ctrl+O reads disk. external = b"% I62_EXTERNAL_BEFORE_REOPEN" From 11f6ba85a69c52ba809a40fb3388beb9ca2b806d Mon Sep 17 00:00:00 2001 From: i-shutov Date: Mon, 2 Nov 2026 22:06:50 +0300 Subject: [PATCH 52/52] qa(I64): real pinned C-backed inspector draft Apply/Undo in N1 X11 vertical --- CHANGELOG.md | 4 ++ .../2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md | 7 +++ ...1-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md | 7 +++ scripts/qa_i61_b42_studio_x11.py | 47 +++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bceda37..0158d3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased — I64 real pinned Inspector Apply → one minimal TikZ source edit → Undo (PENDING) + +- Extend the SAME I61 hosted Linux/X11 six gestures/Save/Open/CRLF/F1/Undo/XeLaTeX script with real selected Node property TextEdit and pinned shared-C Apply, prove draft-only source integrity, one-command patch and exact visual Undo. No new workflow/PR/B code. I63 earlier head SUCCESS and original 64/64 SHA/manual reviewed; new I64 head acceptance pending, physical Win10/M1/M4 NOT_RUN. + ## Unreleased — I63 dual B42 source-tree lease & real File Open focus snapshots - Pin workflow + script to current B42 app/source Git tree `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`; reject mismatches before TeX Live installation. Capture real focus/Ctrl+A/paste screenshots while retaining all six native gestures, source/Undo/Open/PDF assertions. NOT_RUN full GUI and physical hosts until exact-head artifact audit. diff --git a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md index 5685cc90..13fdb1eb 100644 --- a/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md +++ b/insights/2026-11-02_I61_B42_STUDIO_REAL_X11_QA.md @@ -56,3 +56,10 @@ One stacked integration commit combines B's three-file change with the existing ## I63 QA double-lease correction — 2026-11-02 The integrator caught an additional historical B app-tree SHA hardcoded in the Python real-X11 driver (not only the workflow), which would reject the correct B42 executable before GUI. Both now bind exact `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`; lightweight CI preflight asserts equality BEFORE installing TeX Live. Added screenshots immediately after File TextEdit pointer focus and Ctrl+A to distinguish focus from clipboard paste if two-file Open fails. Same stacked QA #92 only, no B edits/new PR. Prior first I63 head is superseded on this exact issue; any CI failure from that old head is stale QA rather than demonstrated product regression. Complete Open/Undo/Apply and physical Win10/M1/M4 remain unaccepted until evidence. + + +## I64 exact B42 pinned Inspector Apply complete-vertical gate + +I63 [real Linux/X11 41505873672](https://github.com/i-shutov/tikzarium/actions/runs/41505873672) completed SUCCESS on exact D QA HEAD `74b67c1b77a7071b2ea4ca6eae4e321e6a28f5d2` and frozen B app/source tree `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`: GUI File path TextEdit stayed open, loaded independent CRLF/opaque second .tex, GUI edit/save/reload, dirty Open cancellation, six native gestures, F1/Undo and external XeLaTeX PDF. Independent original artifact 13170508997 verified 64/64 SHA-256, full File/Open/dirty modal/F1 PNGs and PDF glyphs/font embedding visually reviewed. This is a concrete accepted hosted Linux/X11 subvertical, not physical Win10/M1/M4. + +To finish the remaining N1 gap on the same frozen B production tree, existing I61 script now drives actual selected Node #6 `x` TextEdit with a different `10.50cm` value, asserts draft-only Save leaves source bytes unchanged, real pinned `Применить параметры` writes exactly one C-owned Node command/metadata source line, and real inspector visual Undo restores the saved file byte-exactly. New original screenshots and SHA ledger required, no direct Rust Session fixture or `UNIT_EVENT` accepted as replacement. New exact-head CI and independent artefact manual review are PENDING. No B production edits or additional PR/workflows; physical Win10/MiKTeX and Mac ARM XeLaTeX still NOT_RUN. diff --git a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md index b965d7c6..4e1a2f44 100644 --- a/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md +++ b/plans/2026-11-02_I61_B42_STUDIO_REAL_X11_ACCEPTANCE.md @@ -56,3 +56,10 @@ One stacked integration commit combines B's three-file change with the existing ## I63 QA double-lease correction — 2026-11-02 The integrator caught an additional historical B app-tree SHA hardcoded in the Python real-X11 driver (not only the workflow), which would reject the correct B42 executable before GUI. Both now bind exact `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`; lightweight CI preflight asserts equality BEFORE installing TeX Live. Added screenshots immediately after File TextEdit pointer focus and Ctrl+A to distinguish focus from clipboard paste if two-file Open fails. Same stacked QA #92 only, no B edits/new PR. Prior first I63 head is superseded on this exact issue; any CI failure from that old head is stale QA rather than demonstrated product regression. Complete Open/Undo/Apply and physical Win10/M1/M4 remain unaccepted until evidence. + + +## I64 exact B42 pinned Inspector Apply complete-vertical gate + +I63 [real Linux/X11 41505873672](https://github.com/i-shutov/tikzarium/actions/runs/41505873672) completed SUCCESS on exact D QA HEAD `74b67c1b77a7071b2ea4ca6eae4e321e6a28f5d2` and frozen B app/source tree `72499c6e31d4cf6a2a36d16fb7bc8b10a9d66acb`: GUI File path TextEdit stayed open, loaded independent CRLF/opaque second .tex, GUI edit/save/reload, dirty Open cancellation, six native gestures, F1/Undo and external XeLaTeX PDF. Independent original artifact 13170508997 verified 64/64 SHA-256, full File/Open/dirty modal/F1 PNGs and PDF glyphs/font embedding visually reviewed. This is a concrete accepted hosted Linux/X11 subvertical, not physical Win10/M1/M4. + +To finish the remaining N1 gap on the same frozen B production tree, existing I61 script now drives actual selected Node #6 `x` TextEdit with a different `10.50cm` value, asserts draft-only Save leaves source bytes unchanged, real pinned `Применить параметры` writes exactly one C-owned Node command/metadata source line, and real inspector visual Undo restores the saved file byte-exactly. New original screenshots and SHA ledger required, no direct Rust Session fixture or `UNIT_EVENT` accepted as replacement. New exact-head CI and independent artefact manual review are PENDING. No B production edits or additional PR/workflows; physical Win10/MiKTeX and Mac ARM XeLaTeX still NOT_RUN. diff --git a/scripts/qa_i61_b42_studio_x11.py b/scripts/qa_i61_b42_studio_x11.py index 98dff963..d404b333 100644 --- a/scripts/qa_i61_b42_studio_x11.py +++ b/scripts/qa_i61_b42_studio_x11.py @@ -304,6 +304,53 @@ def main() -> int: raise RuntimeError("Real F1 failed Properties -> Help -> Properties tab transition") report["f1_two_state"] = "PASS_REAL_X11_TAB_CHANGE_MANUAL_LABEL_REVIEW_REQUIRED" + # I64: an actual GUI Inspector edit, not a headless Rust event or + # a direct Session patch. Node #6 is selected; use one shared-C draft. + inspector_before = source.read_bytes() + before_items = owners(inspector_before) + if len(before_items) != 6 or before_items[-1]["tool"] != "node": + raise RuntimeError("Inspector Apply requires selected native Node #6") + screenshot(out, "28a-inspector-before-draft") + click(found, 1080, 560) # Actual selected Node x TextEdit in Properties dock. + key("ctrl+a") + paste(b"10.50cm", out / "clipboard-inspector-node-x.txt") + screenshot(out, "28b-inspector-draft-only") + click(found, 177, 39) # GUI Save must NOT apply an inspector draft. + if source.read_bytes() != inspector_before: + raise RuntimeError("Inspector draft unexpectedly changed saved source") + click(found, 1235, 157) # Actual pinned 'Применить параметры' control. + screenshot(out, "28c-inspector-applied") + click(found, 177, 39) + wait(lambda: source.read_bytes() != inspector_before, + "Real pinned Inspector Apply did not persist a source patch") + inspector_applied = source.read_bytes() + before_lines = inspector_before.splitlines(keepends=True) + after_lines = inspector_applied.splitlines(keepends=True) + if len(before_lines) != len(after_lines): + raise RuntimeError("Inspector Apply changed source line count") + altered = [i for i, (left, right) in enumerate(zip(before_lines, after_lines)) + if left != right] + if ( + len(altered) != 1 + or b"\\node at (10.50cm,5.00cm)" not in after_lines[altered[0]] + or b'"x":"10.50cm"' not in after_lines[altered[0]] + or len(owners(inspector_applied)) != 6 + or not original_preserved(fixture, inspector_applied) + ): + raise RuntimeError("Inspector Apply did not make a minimal C-owned Node x patch") + report["steps"].append({ + "case": "real_pinned_inspector_node_x_apply_minimal_source_patch", + "before_sha256": hashlib.sha256(inspector_before).hexdigest(), + "after_sha256": sha(source), + "changed_line_number": altered[0] + 1, + }) + click(found, 1085, 157) # Same real inspector's visual Undo. + screenshot(out, "28d-inspector-apply-undo") + click(found, 177, 39) + wait(lambda: source.read_bytes() == inspector_before, + "Visual Undo after pinned Inspector Apply failed byte-exact source restore") + report["inspector_apply"] = "PASS_REAL_X11_C_CATALOGUE_NODE_X_APPLY_AND_UNDO" + # TextEdit Ctrl+Z and Session visual inverse-patch Undo are different. six_bytes = source.read_bytes() manual = b"% I62_NATIVE_UNDO_MARKER"