mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e8165674e | |||
| e3caced67b | |||
| d996441241 | |||
| 8a40998c2a | |||
| 924585be0a | |||
| b2c0fd71d0 | |||
| 2605261665 | |||
| 397bbf38c7 | |||
| 4db733ec68 |
Generated
+8
@@ -4366,6 +4366,7 @@ dependencies = [
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"solidcraft-space-mouse",
|
||||
"solidcraft-ui-registry",
|
||||
"syn 2.0.119",
|
||||
"sys-locale",
|
||||
"tiny-skia",
|
||||
@@ -4377,6 +4378,13 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-registry"
|
||||
version = "0.40.0-rc.3"
|
||||
dependencies = [
|
||||
"egui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.40.0-rc.3"
|
||||
|
||||
@@ -28,6 +28,7 @@ solidcraft-doc = { path = "crates/doc" }
|
||||
solidcraft-io = { path = "crates/io" }
|
||||
solidcraft-engine = { path = "crates/engine" }
|
||||
solidcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
solidcraft-ui-registry = { path = "crates/ui-registry" }
|
||||
solidcraft-space-mouse = { path = "crates/space-mouse" }
|
||||
solidcraft-mcp = { path = "crates/mcp" }
|
||||
solidcraft-derive = { path = "crates/derive" }
|
||||
|
||||
@@ -6,48 +6,29 @@ use std::collections::HashMap;
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec2;
|
||||
use solidcraft_io::{DxfEntity, DxfLayer, Geom2, MAX_DRAWING_BYTES};
|
||||
use solidcraft_sketch::{Analysis, CurveKind, LinkKind, LinkSource, Sketch};
|
||||
use solidcraft_sketch::{CurveKind, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::sketch::{Left, edit_solved, ids_of};
|
||||
use super::sketch::{edit, ids_of};
|
||||
use super::sketch_store;
|
||||
use crate::params::{bad, num, str_, vec2};
|
||||
use crate::schema::sketch_import::{DrawingInserted, DxfExported, ExportDxfParams, ExportDxfResult, InsertDxfParams, InsertSvgParams, SketchSolve};
|
||||
use crate::schema::{TypedContract, output_schema_for};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
const INSERT_DXF_CONTRACT: TypedContract<InsertDxfParams> = TypedContract::new();
|
||||
const INSERT_SVG_CONTRACT: TypedContract<InsertSvgParams> = TypedContract::new();
|
||||
const EXPORT_DXF_CONTRACT: TypedContract<ExportDxfParams> = TypedContract::new();
|
||||
|
||||
/// A command's reply as JSON, built from the type its output schema describes so the two can't
|
||||
/// drift apart.
|
||||
fn reply(cmd: &str, reply: impl serde::Serialize) -> Result<Value> {
|
||||
serde_json::to_value(reply).map_err(|e| bad(cmd, e.to_string()))
|
||||
}
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("sketch.insert_dxf", "Insert DXF", crate::checked!("sketch.insert_dxf", insert_dxf))
|
||||
CommandSpec::new("sketch.insert_dxf", "Insert DXF", insert_dxf)
|
||||
.at("SKETCH", "INSERT")
|
||||
.icon("sketch_dxf")
|
||||
.contract(&INSERT_DXF_CONTRACT)
|
||||
.output(output_schema_for::<DrawingInserted>)
|
||||
.params(concat!("path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale? | width? | height?: mm, the drawing's size before rotation, aspect kept (win over scale); angle?: degrees, about the drawing's origin (or its centre); align?: \"origin\" (default: `at` is where the file's origin goes) | \"center\" (`at` is where the drawing's middle goes and it turns and scales about it); tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())),
|
||||
CommandSpec::new("sketch.insert_svg", "Insert SVG", crate::checked!("sketch.insert_svg", insert_svg))
|
||||
CommandSpec::new("sketch.insert_svg", "Insert SVG", insert_svg)
|
||||
.at("SKETCH", "INSERT")
|
||||
.icon("svg")
|
||||
.contract(&INSERT_SVG_CONTRACT)
|
||||
.output(output_schema_for::<DrawingInserted>)
|
||||
.params(concat!("path | text: SVG; at?: [x,y] offset, scale? | width? | height?: mm, the drawing's size before rotation, aspect kept (win over scale); angle?: degrees, about the drawing's origin (or its centre); align?: \"origin\" (default: `at` is where the file's origin goes) | \"center\" (`at` is where the drawing's middle goes and it turns and scales about it); tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)", created_doc!())),
|
||||
CommandSpec::new("sketch.drawing_bounds", "Drawing Size", drawing_bounds)
|
||||
.icon("svg")
|
||||
.noundo()
|
||||
.params("path | text: SVG or DXF (by `format`: \"svg\" | \"dxf\", default by the file name, else SVG); returns min, max, width and height in the file's own size"),
|
||||
CommandSpec::new("sketch.export_dxf", "Save As DXF", crate::checked!("sketch.export_dxf", export_dxf))
|
||||
.icon("sketch_dxf")
|
||||
CommandSpec::new("sketch.export_dxf", "Save As DXF", export_dxf).icon("sketch_dxf")
|
||||
.noundo()
|
||||
.contract(&EXPORT_DXF_CONTRACT)
|
||||
.output(output_schema_for::<ExportDxfResult>)
|
||||
.params("sketch?: id|name (default active); path?: file to write (else the DXF text is returned); units?: \"design\" | \"in\" | \"inch\" | \"mm\" | \"cm\" | \"m\" | \"ft\" (default mm); layer?: profile layer name (default \"0\"); layers?: \"single\" | \"roles\" (default \"single\": all on `layer`; \"roles\": outer on \"OUTLINE\", holes on \"CUT\"); join?: bool (default false: R12 LINE/ARC; true: closed loops as LWPOLYLINE with arc bulges)"),
|
||||
];
|
||||
|
||||
@@ -270,13 +251,10 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
||||
if geom.len() > solidcraft_sketch::MAX_POINTS {
|
||||
return Err(solidcraft_sketch::SketchError::TooLarge.into());
|
||||
}
|
||||
// `sketch::edit`, but keeping the solve as a typed value for the reply.
|
||||
let target = sketch_store::target(s, p, cmd)?;
|
||||
let ((curves, texts), report) = edit_solved(s, target, false, Analysis::Freedom, |sk, _| {
|
||||
let ((curves, texts), info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let (cs, texts) = add_geometry(sk, &geom, &f, k, tol)?;
|
||||
Ok(((ids_of(sk, &cs), texts), Left::Unsolved))
|
||||
Ok((ids_of(sk, &cs), texts))
|
||||
})?;
|
||||
let info = report.map(|rep| SketchSolve { solved: rep.keeps_orientation(), dof: rep.dof });
|
||||
// Text becomes sketch text.
|
||||
let id = sketch_store::active(s).ok_or_else(|| bad(cmd, "no sketch"))?;
|
||||
let mut n_text = 0;
|
||||
@@ -301,7 +279,7 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
reply(cmd, DrawingInserted { sketch: id, created_sketch: created, curves: curves.len(), texts: n_text, info, param_warnings: Vec::new() })
|
||||
Ok(json!({"sketch": id, "created_sketch": created, "curves": curves.len(), "texts": n_text, "info": info}))
|
||||
}
|
||||
|
||||
fn insert_dxf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -544,10 +522,9 @@ fn export_dxf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
if let Some(path) = str_(p, "path").filter(|x| !x.trim().is_empty()) {
|
||||
solidcraft_io::vfs::write(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let exported = DxfExported::Written { path: path.to_string(), entities: entity_count };
|
||||
return reply(cmd, ExportDxfResult { exported, param_warnings: Vec::new() });
|
||||
return Ok(json!({"path": path, "entities": entity_count}));
|
||||
}
|
||||
reply(cmd, ExportDxfResult { exported: DxfExported::Text { dxf: text, entities: entity_count }, param_warnings: Vec::new() })
|
||||
Ok(json!({"dxf": text, "entities": entity_count}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -110,7 +110,6 @@ pub mod sketch_constraints;
|
||||
pub mod sketch_create;
|
||||
pub mod sketch_ends;
|
||||
pub mod sketch_equation;
|
||||
pub mod sketch_import;
|
||||
pub mod sketch_inspect;
|
||||
pub mod sketch_modify;
|
||||
pub mod sketch_project;
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
//! Contracts and result types for the commands in `cmd/sketch_import.rs`: `sketch.insert_dxf`,
|
||||
//! `sketch.insert_svg` and `sketch.export_dxf`.
|
||||
//!
|
||||
//! The handlers read their parameters from the raw JSON, so each key here reads as its handler
|
||||
//! reads it and fails with the handler's message. Whether a file can be read or written, a sketch
|
||||
//! exists or a drawing has anything to import stays the handler's to check. A call with several
|
||||
//! problems may get a different one first in strict than with the check off (the check goes key
|
||||
//! by key, the handlers in their own order).
|
||||
//!
|
||||
//! Two insert keys are read only in some cases, and the contract reads them in the same cases:
|
||||
//! `path` only when `text` is not given, and of `width`, `height` and `scale` only the first one
|
||||
//! given, in that order (a `height` next to a `width`, or a `scale` next to either, is never read).
|
||||
//! A value of the wrong type that the handler reads as if the key were left out (`text: 5` next
|
||||
//! to a good `path`, a text for `scale`) is refused by the contract, as schemas.md has it: that
|
||||
//! difference is what warn and strict report.
|
||||
//! `plane` and `name` are read only when no sketch is being edited, which the check can't see
|
||||
//! (it has no session), so they take any value.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use solidcraft_io::MAX_DRAWING_BYTES;
|
||||
|
||||
use super::check::{located, whole};
|
||||
use super::sketch_ends::SketchTarget;
|
||||
use super::types::{Point2, not_blank_pattern};
|
||||
|
||||
/// A key that, once given (`null` included), must parse as `T`, as the handler reads it.
|
||||
fn some<'de, D: Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
|
||||
T::deserialize(d).map(Some)
|
||||
}
|
||||
|
||||
/// The keys the two insert commands read. See [`InsertDxfParams`] and [`InsertSvgParams`], which
|
||||
/// leave `path`, `height` and `scale` unread in the cases the handler does.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields, transform = source_required)]
|
||||
pub struct InsertDrawingKeys {
|
||||
/// The drawing itself, as text. Wins over `path`. At most 16 MiB (`MAX_DRAWING_BYTES`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<DrawingText>,
|
||||
/// The drawing file to read (not blank, fewer than 4096 UTF-8 bytes). Used when `text` is
|
||||
/// not given; ignored when it is.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "path_schema")]
|
||||
pub path: Option<DrawingPath>,
|
||||
/// An offset, in mm, [x, y]. Default: [0, 0]. With `align` "origin" it is where the file's
|
||||
/// origin goes; with "center", where the drawing's middle goes.
|
||||
#[serde(default)]
|
||||
pub at: Option<Point2>,
|
||||
/// A size factor, 1e-9 to 1e6 (a value outside is read as 1). Default: 1. Used only when
|
||||
/// neither `width` nor `height` is given.
|
||||
#[serde(default)]
|
||||
pub scale: Option<f64>,
|
||||
/// The drawing's width in mm after sizing, before rotation; the aspect ratio is kept. Wins
|
||||
/// over `height` and `scale`. The drawing must have a width.
|
||||
#[serde(default, deserialize_with = "some", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "PositiveMm")]
|
||||
pub width: Option<PositiveMm>,
|
||||
/// The drawing's height in mm after sizing, before rotation; the aspect ratio is kept. Wins
|
||||
/// over `scale`; ignored when `width` is given. The drawing must have a height.
|
||||
#[serde(default, deserialize_with = "some", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "PositiveMm")]
|
||||
pub height: Option<PositiveMm>,
|
||||
/// A turn in degrees, about the drawing's origin (with `align` "center": about its middle).
|
||||
/// Default: 0.
|
||||
#[serde(default, deserialize_with = "some", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "AngleDegrees")]
|
||||
pub angle: Option<AngleDegrees>,
|
||||
/// What `at` places: the file's origin ("origin") or the drawing's middle ("center", which it
|
||||
/// then also turns and scales about). Default: origin.
|
||||
#[serde(default)]
|
||||
pub align: Option<DrawingAlign>,
|
||||
/// The distance in mm within which ends join into one point, 1e-9 to 10 (a value outside is
|
||||
/// brought inside; one that is not above 0 reads as the default). Default: 0.0001.
|
||||
#[serde(default)]
|
||||
pub tolerance: Option<f64>,
|
||||
/// Read only when no sketch is being edited: the plane of the new sketch, as `sketch.create`
|
||||
/// takes it. Default: XY. Ignored (and not checked) while a sketch is being edited.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plane: Option<Value>,
|
||||
/// Read only when no sketch is being edited: the new sketch's name. Ignored (and not checked)
|
||||
/// while a sketch is being edited.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<Value>,
|
||||
/// The sketch to insert into: its feature id or its name. Default: the one being edited (a
|
||||
/// new one when none is).
|
||||
#[serde(default)]
|
||||
pub sketch: Option<SketchTarget>,
|
||||
}
|
||||
|
||||
/// Leaves out the keys the handler never reads for this call: `path` next to a `text` that is
|
||||
/// text, `height` next to a `width`, and `scale` next to either.
|
||||
fn leave_unread(value: &mut Value) {
|
||||
let Some(keys) = value.as_object_mut() else { return };
|
||||
if keys.get("text").is_some_and(Value::is_string) {
|
||||
keys.remove("path");
|
||||
}
|
||||
if keys.contains_key("width") {
|
||||
keys.remove("height");
|
||||
}
|
||||
if keys.contains_key("width") || keys.contains_key("height") {
|
||||
keys.remove("scale");
|
||||
}
|
||||
}
|
||||
|
||||
fn read_keys<'de, D: Deserializer<'de>>(d: D) -> Result<InsertDrawingKeys, D::Error> {
|
||||
let mut value = Value::deserialize(d)?;
|
||||
leave_unread(&mut value);
|
||||
serde_path_to_error::deserialize(&value).map_err(|e| D::Error::custom(located(&e)))
|
||||
}
|
||||
|
||||
/// Insert an ASCII DXF drawing into a sketch as curves: the active sketch, or a new one when none
|
||||
/// is being edited. Give the drawing as `path` or as `text`. Blocks are expanded. It can be
|
||||
/// sized, turned and placed. Text in the drawing becomes sketch text. One undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertDxfParams(pub InsertDrawingKeys);
|
||||
|
||||
impl<'de> Deserialize<'de> for InsertDxfParams {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
read_keys(d).map(InsertDxfParams)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for InsertDxfParams {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"InsertDxfParams".into()
|
||||
}
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema = InsertDrawingKeys::json_schema(generator);
|
||||
schema.insert("description".into(), INSERT_DXF.into());
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
const INSERT_DXF: &str = "Insert an ASCII DXF drawing into a sketch as curves: the active sketch, or a new one when none is being edited. Give the drawing as `path` or as `text`. Blocks are expanded. It can be sized, turned and placed. Text in the drawing becomes sketch text. One undo step.";
|
||||
|
||||
/// Insert an SVG drawing into a sketch as curves: the active sketch, or a new one when none is
|
||||
/// being edited. Give the drawing as `path` or as `text`. It can be sized, turned and placed.
|
||||
/// Text in the drawing becomes sketch text. One undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertSvgParams(pub InsertDrawingKeys);
|
||||
|
||||
impl<'de> Deserialize<'de> for InsertSvgParams {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
read_keys(d).map(InsertSvgParams)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for InsertSvgParams {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"InsertSvgParams".into()
|
||||
}
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema = InsertDrawingKeys::json_schema(generator);
|
||||
schema.insert("description".into(), INSERT_SVG.into());
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
const INSERT_SVG: &str = "Insert an SVG drawing into a sketch as curves: the active sketch, or a new one when none is being edited. Give the drawing as `path` or as `text`. It can be sized, turned and placed. Text in the drawing becomes sketch text. One undo step.";
|
||||
|
||||
/// The insert commands need `path` or `text`; the handler says so when both are left out. A
|
||||
/// `path` it reads (no `text`) is not blank and under 4096 bytes; `maxLength` counts
|
||||
/// characters, so 4095 is as close as the schema gets. `height` is read only without a `width`
|
||||
/// and `scale` only without either, so their shapes apply only then, as [`leave_unread`] has it.
|
||||
fn source_required(schema: &mut Schema) {
|
||||
let read_path = serde_json::json!({"type": "string", "maxLength": 4095, "pattern": not_blank_pattern()});
|
||||
schema.insert("anyOf".into(), serde_json::json!([{"required": ["text"]}, {"required": ["path"], "properties": {"path": read_path}}]));
|
||||
let Some(props) = schema.get_mut("properties").and_then(Value::as_object_mut) else { return };
|
||||
let mut read_only_without = |key: &str| {
|
||||
let shape = props.get_mut(key).map(Value::take).unwrap_or_default();
|
||||
props.insert(key.into(), serde_json::json!({"description": shape.get("description").cloned().unwrap_or_default()}));
|
||||
shape
|
||||
};
|
||||
let height = read_only_without("height");
|
||||
let scale = read_only_without("scale");
|
||||
schema.insert(
|
||||
"allOf".into(),
|
||||
serde_json::json!([
|
||||
{"anyOf": [{"required": ["width"]}, {"properties": {"height": height}}]},
|
||||
{"anyOf": [{"required": ["width"]}, {"required": ["height"]}, {"properties": {"scale": scale}}]},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/// `path` is checked by [`source_required`] only when it is read (no `text`).
|
||||
fn path_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({})
|
||||
}
|
||||
|
||||
/// A drawing's text, as `source_text` reads it: at most 16 MiB, or the handler's refusal.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DrawingText(pub String);
|
||||
|
||||
impl<'de> Deserialize<'de> for DrawingText {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let text = String::deserialize(d)?;
|
||||
if text.len() > MAX_DRAWING_BYTES {
|
||||
return Err(D::Error::custom(whole("the drawing is too large")));
|
||||
}
|
||||
Ok(DrawingText(text))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for DrawingText {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"DrawingText".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": "string"})
|
||||
}
|
||||
}
|
||||
|
||||
/// A drawing file name, as `source_text` reads `path`: not blank and under 4096 bytes, or the
|
||||
/// handler's "give `path` or `text`" (anything but text reads as no `path`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DrawingPath(pub String);
|
||||
|
||||
impl<'de> Deserialize<'de> for DrawingPath {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
match Value::deserialize(d)? {
|
||||
Value::String(s) if !s.trim().is_empty() && s.len() < 4096 => Ok(DrawingPath(s)),
|
||||
_ => Err(D::Error::custom(whole("give `path` or `text`"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A size in mm above 0, as the insert commands' `width` and `height` are read.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PositiveMm(pub f64);
|
||||
|
||||
/// The message the insert commands give for `width` or `height`: the check puts the key first.
|
||||
const POSITIVE_MM: &str = "must be a positive number (mm)";
|
||||
|
||||
impl<'de> Deserialize<'de> for PositiveMm {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
Value::deserialize(d)?.as_f64().filter(|w| w.is_finite() && *w > 0.0).map(PositiveMm).ok_or_else(|| D::Error::custom(POSITIVE_MM))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for PositiveMm {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"PositiveMm".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"description": "A size in mm, above 0.", "type": "number", "exclusiveMinimum": 0})
|
||||
}
|
||||
}
|
||||
|
||||
/// An angle in degrees as `angle` is read: a number, whatever its size.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AngleDegrees(pub f64);
|
||||
|
||||
impl<'de> Deserialize<'de> for AngleDegrees {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
Value::deserialize(d)?.as_f64().filter(|a| a.is_finite()).map(AngleDegrees).ok_or_else(|| D::Error::custom("must be a number (degrees)"))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AngleDegrees {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AngleDegrees".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"description": "An angle in degrees.", "type": "number"})
|
||||
}
|
||||
}
|
||||
|
||||
/// What an insert's `at` places, as `align` is read (the words are case-sensitive).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DrawingAlign {
|
||||
/// `at` is where the file's origin goes.
|
||||
Origin,
|
||||
/// `at` is where the drawing's middle goes, and it turns and scales about it.
|
||||
Center,
|
||||
}
|
||||
|
||||
impl JsonSchema for DrawingAlign {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"DrawingAlign".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "What `at` places: \"origin\" (the file's origin goes there) or \"center\" (the drawing's middle goes there, and it turns and scales about it). Lower case only.",
|
||||
"type": "string",
|
||||
"enum": ["origin", "center"]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DrawingAlign {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
match Value::deserialize(d)?.as_str() {
|
||||
Some("origin") => Ok(DrawingAlign::Origin),
|
||||
Some("center") => Ok(DrawingAlign::Center),
|
||||
_ => Err(D::Error::custom("must be \"origin\" or \"center\"")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `sketch.insert_dxf` and `sketch.insert_svg` return.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct DrawingInserted {
|
||||
/// The sketch the drawing went into, by feature id.
|
||||
pub sketch: u64,
|
||||
/// The new sketch's id when no sketch was being edited and one was made; else null.
|
||||
pub created_sketch: Option<u64>,
|
||||
/// How many curves were added.
|
||||
pub curves: usize,
|
||||
/// How many sketch texts were added.
|
||||
pub texts: usize,
|
||||
/// The sketch's solve afterwards; null when it was not solved.
|
||||
pub info: Option<SketchSolve>,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// A sketch's solve, as the sketch commands report it.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct SketchSolve {
|
||||
/// Whether the solve kept the sketch's orientation.
|
||||
pub solved: bool,
|
||||
/// The degrees of freedom left (0 = fully constrained).
|
||||
pub dof: usize,
|
||||
}
|
||||
|
||||
/// Save a sketch's curves as an ASCII DXF: to the file `path`, or, without it, as text in the
|
||||
/// reply. Construction curves are left out; splines and other curves are written as line
|
||||
/// segments.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ExportDxfParams {
|
||||
/// The sketch: its feature id or its name. Default: the one being edited.
|
||||
#[serde(default)]
|
||||
pub sketch: Option<SketchTarget>,
|
||||
/// The file to write. Default (also when blank): none, and the DXF text is returned.
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
/// The drawing's units (any case): `design` (the design's length unit), `mm`, `in` or
|
||||
/// `inch`, `cm`, `m` or `ft`. Default: mm.
|
||||
#[serde(default, deserialize_with = "dxf_units")]
|
||||
#[schemars(schema_with = "dxf_units_schema")]
|
||||
pub units: Option<String>,
|
||||
/// The layer of the profiles (blank: the default). Default: `0`.
|
||||
#[serde(default, deserialize_with = "dxf_layer")]
|
||||
#[schemars(schema_with = "dxf_layer_schema")]
|
||||
pub layer: Option<String>,
|
||||
/// `single` (any case, the default): everything on `layer`; `roles`: outer loops on
|
||||
/// `OUTLINE` and holes on `CUT`.
|
||||
#[serde(default, deserialize_with = "dxf_layers")]
|
||||
#[schemars(schema_with = "dxf_layers_schema")]
|
||||
pub layers: Option<String>,
|
||||
/// `true` writes closed loops as LWPOLYLINE with arc bulges. Default: false (LINE and ARC).
|
||||
#[serde(default, deserialize_with = "dxf_join")]
|
||||
pub join: Option<bool>,
|
||||
}
|
||||
|
||||
/// The units `sketch.export_dxf` takes, in lower case (the handler reads them in any case).
|
||||
const DXF_UNITS: [&str; 7] = ["design", "mm", "in", "inch", "cm", "m", "ft"];
|
||||
|
||||
fn dxf_units<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(units)) => {
|
||||
let lower = units.to_ascii_lowercase();
|
||||
if DXF_UNITS.contains(&lower.as_str()) {
|
||||
Ok(Some(lower))
|
||||
} else {
|
||||
Err(D::Error::custom(whole(format!("unsupported unit `{lower}` (use \"design\", \"mm\", \"in\", \"cm\", \"m\", or \"ft\")"))))
|
||||
}
|
||||
}
|
||||
Some(_) => Err(D::Error::custom(whole("`units` must be a string"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dxf_units_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": ["string", "null"], "description": "design, mm, in, inch, cm, m or ft, in any case. Default: mm."})
|
||||
}
|
||||
|
||||
fn dxf_layer<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(layer)) => Ok(Some(layer)),
|
||||
Some(_) => Err(D::Error::custom(whole("`layer` must be a string"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dxf_layer_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": ["string", "null"], "description": "The profiles' layer (blank: the default). Default: 0."})
|
||||
}
|
||||
|
||||
fn dxf_layers<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(layers)) if ["single", "roles"].contains(&layers.to_ascii_lowercase().as_str()) => Ok(Some(layers.to_ascii_lowercase())),
|
||||
Some(_) => Err(D::Error::custom(whole("`layers` must be \"single\" or \"roles\""))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dxf_layers_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": ["string", "null"], "description": "single or roles, in any case. Default: single."})
|
||||
}
|
||||
|
||||
fn dxf_join<'de, D: Deserializer<'de>>(d: D) -> Result<Option<bool>, D::Error> {
|
||||
match Option::<Value>::deserialize(d)? {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::Bool(join)) => Ok(Some(join)),
|
||||
Some(_) => Err(D::Error::custom(whole("`join` must be true or false"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// What `sketch.export_dxf` returns.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ExportDxfResult {
|
||||
#[serde(flatten)]
|
||||
pub exported: DxfExported,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Where the DXF went.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum DxfExported {
|
||||
/// With a `path`: the file was written.
|
||||
Written {
|
||||
/// The file written, as given.
|
||||
path: String,
|
||||
/// How many entities it holds.
|
||||
entities: usize,
|
||||
},
|
||||
/// Without one: the DXF as text.
|
||||
Text {
|
||||
/// The DXF file's text.
|
||||
dxf: String,
|
||||
/// How many entities it holds.
|
||||
entities: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_import_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,313 +0,0 @@
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_io::MAX_DRAWING_BYTES;
|
||||
|
||||
use super::{DrawingInserted, ExportDxfParams, ExportDxfResult, InsertDxfParams, InsertSvgParams};
|
||||
use crate::Session;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
use crate::schema::{ParamCheck, input_schema_for, output_schema_for};
|
||||
|
||||
/// A 30 x 20 rectangle as DXF lines.
|
||||
const DXF: &str = "0\nSECTION\n2\nENTITIES\n0\nLINE\n10\n0\n20\n0\n11\n30\n21\n0\n0\nLINE\n10\n30\n20\n0\n11\n30\n21\n20\n0\nLINE\n10\n30\n20\n20\n11\n0\n21\n20\n0\nLINE\n10\n0\n20\n20\n11\n0\n21\n0\n0\nENDSEC\n0\nEOF\n";
|
||||
|
||||
const SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="20mm" height="10mm" viewBox="0 0 20 10"><rect x="0" y="0" width="20" height="10"/><text x="2" y="5" font-size="3">Hi</text></svg>"#;
|
||||
|
||||
fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-12508-{tag}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_the_key_check_agree() {
|
||||
let insert = ["align", "angle", "at", "height", "name", "path", "plane", "scale", "sketch", "text", "tolerance", "width"];
|
||||
assert_keys(input_schema_for::<InsertDxfParams>(), &insert);
|
||||
assert_keys(input_schema_for::<InsertSvgParams>(), &insert);
|
||||
assert_keys(input_schema_for::<ExportDxfParams>(), &["join", "layer", "layers", "path", "sketch", "units"]);
|
||||
}
|
||||
|
||||
/// Strict gives the handlers' messages for the calls they refuse for a key (a missing `path` and
|
||||
/// `text` included: the handler's words, not "is required"). The session has a sketch being edited.
|
||||
#[test]
|
||||
fn strict_keeps_todays_errors() {
|
||||
let too_big = "x".repeat(MAX_DRAWING_BYTES + 1);
|
||||
let mut cases: Vec<(&str, Value)> = vec![
|
||||
("sketch.insert_dxf", json!({})),
|
||||
("sketch.insert_dxf", json!({"path": ""})),
|
||||
("sketch.insert_dxf", json!({"path": " "})),
|
||||
("sketch.insert_dxf", json!({"path": 5})),
|
||||
("sketch.insert_dxf", json!({"path": null})),
|
||||
("sketch.insert_dxf", json!({"path": "x".repeat(4096)})),
|
||||
("sketch.insert_dxf", json!({"text": too_big})),
|
||||
("sketch.insert_svg", json!({})),
|
||||
("sketch.insert_svg", json!({"path": " "})),
|
||||
("sketch.insert_svg", json!({"text": null})),
|
||||
("sketch.export_dxf", json!({"sketch": true})),
|
||||
("sketch.export_dxf", json!({"sketch": [1]})),
|
||||
("sketch.export_dxf", json!({"sketch": -1})),
|
||||
("sketch.export_dxf", json!({"sketch": 1.5})),
|
||||
];
|
||||
for (id, text) in [("sketch.insert_dxf", DXF), ("sketch.insert_svg", SVG)] {
|
||||
for extra in [
|
||||
json!({"width": 0}),
|
||||
json!({"width": -2}),
|
||||
json!({"width": null}),
|
||||
json!({"width": "wide"}),
|
||||
json!({"width": [1]}),
|
||||
json!({"height": 0}),
|
||||
json!({"height": null}),
|
||||
json!({"height": true}),
|
||||
json!({"angle": "x"}),
|
||||
json!({"angle": null}),
|
||||
json!({"angle": {}}),
|
||||
json!({"align": "left"}),
|
||||
json!({"align": "Center"}),
|
||||
json!({"align": ""}),
|
||||
json!({"sketch": true}),
|
||||
json!({"sketch": null, "width": 0}),
|
||||
json!({"sketch": [1]}),
|
||||
json!({"sketch": 1.5}),
|
||||
] {
|
||||
let mut p = json!({"text": text});
|
||||
p.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
||||
cases.push((id, p));
|
||||
}
|
||||
}
|
||||
assert_strict_keeps_todays_errors(&cases);
|
||||
}
|
||||
|
||||
/// The same refusals for a sketch that is named but doesn't exist, a width the drawing can't
|
||||
/// give (a drawing with no width to size is refused by the handler alone), and the export's
|
||||
/// options, which the handler reads only once a sketch is being edited.
|
||||
#[test]
|
||||
fn strict_keeps_the_handlers_own_refusals() {
|
||||
// A vertical line has no width to size.
|
||||
let vertical = "0\nSECTION\n2\nENTITIES\n0\nLINE\n10\n0\n20\n0\n11\n0\n21\n9\n0\nENDSEC\n0\nEOF\n";
|
||||
for (id, p) in [
|
||||
("sketch.insert_dxf", json!({"text": DXF, "sketch": "Nope"})),
|
||||
("sketch.insert_dxf", json!({"text": DXF, "sketch": 9999})),
|
||||
("sketch.insert_dxf", json!({"text": vertical, "width": 10})),
|
||||
("sketch.insert_dxf", json!({"text": "0\nEOF\n"})),
|
||||
("sketch.export_dxf", json!({"sketch": "Nope"})),
|
||||
("sketch.export_dxf", json!({"sketch": 9999})),
|
||||
("sketch.export_dxf", json!({"units": 5})),
|
||||
("sketch.export_dxf", json!({"units": "Yard"})),
|
||||
("sketch.export_dxf", json!({"layer": 1})),
|
||||
("sketch.export_dxf", json!({"layers": "all"})),
|
||||
("sketch.export_dxf", json!({"layers": true})),
|
||||
("sketch.export_dxf", json!({"join": "yes"})),
|
||||
] {
|
||||
let error = |level: ParamCheck| {
|
||||
let mut s = Session { param_check: level, ..Session::default() };
|
||||
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
||||
s.execute(id, &p).err().map(|e| e.to_string())
|
||||
};
|
||||
let off = error(ParamCheck::Off);
|
||||
assert!(off.is_some(), "{id} {p} should fail today");
|
||||
assert_eq!(error(ParamCheck::Strict), off, "{id} {p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The types read each key as the handler does: keys it never reads in a call are not read by the
|
||||
/// contract either, and the ones it reads fail with its words.
|
||||
#[test]
|
||||
fn the_types_read_like_the_handler() {
|
||||
let reads = |p: Value| serde_json::from_value::<InsertDxfParams>(p).is_ok();
|
||||
let issues = |p: Value| crate::schema::issues("sketch.insert_svg", &p).into_iter().map(|i| i.message).collect::<Vec<_>>();
|
||||
// `path` is not read when `text` is given; `height` not when `width` is; `scale` not when
|
||||
// either is.
|
||||
assert!(reads(json!({"text": "x", "path": 5})) && reads(json!({"text": "x", "path": ""})));
|
||||
assert!(reads(json!({"width": 5, "height": "x", "scale": "y"})) && reads(json!({"width": 5, "height": null})));
|
||||
assert!(reads(json!({"height": 5, "scale": "y"})) && reads(json!({"height": 5, "scale": null})));
|
||||
assert!(reads(json!({"path": "a.dxf", "scale": 2, "tolerance": 0.01, "at": [1, 2], "angle": 90, "align": "center"})));
|
||||
assert!(reads(json!({"text": "x", "at": {"x": 1, "y": 2}, "angle": 720.5, "width": 1e12, "scale": 0, "tolerance": -1})));
|
||||
// They are read when the handler reads them.
|
||||
// Stricter than the handler, which reads a `text` that isn't text as left out.
|
||||
assert!(!reads(json!({"text": 5, "path": "a.dxf"})) && !reads(json!({"path": "a.dxf", "scale": "y"})));
|
||||
assert!(!reads(json!({"path": ""})) && !reads(json!({"path": 5, "scale": 1})));
|
||||
assert!(!reads(json!({"text": "x", "height": 0})) && !reads(json!({"text": "x", "height": "x", "scale": 2})));
|
||||
assert!(!reads(json!({"text": "x", "width": null, "height": 5})) && !reads(json!({"text": "x", "scale": "y", "width": 0})));
|
||||
// Only while no sketch is being edited does `plane` or `name` matter, which the check can't
|
||||
// see: they take anything.
|
||||
assert!(reads(json!({"text": "x", "plane": 5, "name": [1]})) && reads(json!({"text": "x", "plane": null, "name": null})));
|
||||
assert_eq!(issues(json!({"text": "x", "width": 0})), ["`width` must be a positive number (mm)"]);
|
||||
assert_eq!(issues(json!({"text": "x", "height": -1})), ["`height` must be a positive number (mm)"]);
|
||||
assert_eq!(issues(json!({"text": "x", "angle": "1"})), ["`angle` must be a number (degrees)"]);
|
||||
assert_eq!(issues(json!({"text": "x", "align": "left"})), ["`align` must be \"origin\" or \"center\""]);
|
||||
assert_eq!(issues(json!({"path": " "})), ["give `path` or `text`"]);
|
||||
assert_eq!(issues(json!({"text": "x", "sketch": true})), ["`sketch` must be an id or a name"]);
|
||||
assert_eq!(issues(json!({"text": "x", "wdth": 5})), ["unknown key `wdth`; did you mean `width`?"]);
|
||||
assert!(issues(json!({"text": "x", "width": 5, "height": "x", "path": 5})).is_empty());
|
||||
let export = |p: Value| serde_json::from_value::<ExportDxfParams>(p).is_ok();
|
||||
assert!(export(json!({})) && export(json!({"sketch": null, "path": null})) && export(json!({"sketch": "S", "path": ""})));
|
||||
assert!(export(json!({"sketch": 3, "path": "a.dxf"})) && !export(json!({"sketch": true})) && !export(json!({"path": 5})));
|
||||
}
|
||||
|
||||
/// What the schemas take: a drawing from `path` or `text`, and the keys' own shapes.
|
||||
#[test]
|
||||
fn the_schemas_take_what_the_handlers_read() {
|
||||
let takes = |schema: schemars::Schema, cases: &[(Value, bool)]| {
|
||||
let schema = schema.as_value();
|
||||
for (p, takes) in cases {
|
||||
assert_eq!(conforms(schema, schema, p), *takes, "{p}");
|
||||
}
|
||||
};
|
||||
for schema in [input_schema_for::<InsertDxfParams>(), input_schema_for::<InsertSvgParams>()] {
|
||||
takes(
|
||||
schema,
|
||||
&[
|
||||
(json!({"text": "x"}), true),
|
||||
(json!({"path": "a.svg", "at": [1, 2], "scale": 2, "angle": 45, "align": "center", "tolerance": 0.01}), true),
|
||||
(json!({"text": "x", "width": 40, "plane": "XZ", "name": "Logo", "sketch": "Sketch1"}), true),
|
||||
(json!({"text": "x", "plane": {"origin": [0, 0, 1], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "sketch": 4}), true),
|
||||
(json!({}), false),
|
||||
(json!({"width": 40}), false),
|
||||
(json!({"text": "x", "width": 0}), false),
|
||||
(json!({"text": "x", "height": -1}), false),
|
||||
(json!({"text": "x", "angle": "x"}), false),
|
||||
(json!({"text": "x", "align": "left"}), false),
|
||||
(json!({"text": "x", "at": [1]}), false),
|
||||
(json!({"text": "x", "wdth": 5}), false),
|
||||
(json!({"path": "x".repeat(4096)}), false),
|
||||
(json!({"text": "x", "path": " "}), true),
|
||||
// Unread keys take anything, as in the handler; read ones keep their shapes.
|
||||
(json!({"text": "x", "path": 5}), true),
|
||||
(json!({"text": "x", "width": 5, "height": -1}), true),
|
||||
(json!({"text": "x", "width": 5, "scale": "y"}), true),
|
||||
(json!({"text": "x", "height": 5, "scale": "y"}), true),
|
||||
(json!({"path": 5}), false),
|
||||
(json!({"text": "x", "height": "x", "scale": 2}), false),
|
||||
(json!({"text": "x", "scale": "y"}), false),
|
||||
],
|
||||
);
|
||||
}
|
||||
// A `path` read without `text` is not blank (`conforms` doesn't check patterns).
|
||||
for schema in [input_schema_for::<InsertDxfParams>(), input_schema_for::<InsertSvgParams>()] {
|
||||
let read_path = &schema.as_value()["anyOf"][1]["properties"]["path"];
|
||||
assert_eq!(read_path["pattern"], json!(crate::schema::types::not_blank_pattern()));
|
||||
}
|
||||
takes(
|
||||
input_schema_for::<ExportDxfParams>(),
|
||||
&[
|
||||
(json!({}), true),
|
||||
(json!({"sketch": "S", "path": "a.dxf"}), true),
|
||||
(json!({"sketch": 2}), true),
|
||||
(json!({"sketch": true}), false),
|
||||
(json!({"file": "a.dxf"}), false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
fn matches(schema: schemars::Schema, r: &Value) {
|
||||
let schema = schema.as_value();
|
||||
assert!(conforms(schema, schema, r), "{r} doesn't match {schema}");
|
||||
}
|
||||
|
||||
/// Every form of the three commands replies as its output schema says, in Warn without warnings;
|
||||
/// the replies carry the keys they always had; a misspelt key gets its warning and the reply still
|
||||
/// matches, and strict refuses it.
|
||||
#[test]
|
||||
fn replies_match_the_output_schema() {
|
||||
let dir = temp_dir("replies");
|
||||
let svg_file = dir.join("plate.svg");
|
||||
let dxf_file = dir.join("plate.dxf");
|
||||
std::fs::write(&svg_file, SVG).unwrap();
|
||||
std::fs::write(&dxf_file, DXF).unwrap();
|
||||
let (svg_path, dxf_path) = (svg_file.to_string_lossy().into_owned(), dxf_file.to_string_lossy().into_owned());
|
||||
let mut s = Session { param_check: ParamCheck::Warn, ..Session::default() };
|
||||
let run = |s: &mut Session, id: &str, p: Value| {
|
||||
let r = s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"));
|
||||
assert!(r.get("param_warnings").is_none(), "{id} {p}: {r}");
|
||||
r
|
||||
};
|
||||
// The session adds `created` (the ids a sketch command made) to every `sketch.*` reply, outside
|
||||
// the handlers; it is not part of what they build.
|
||||
let keys = |r: &Value| {
|
||||
r.as_object().map(|o| o.keys().filter(|k| *k != "created").cloned().collect::<std::collections::BTreeSet<_>>()).unwrap_or_default()
|
||||
};
|
||||
let insert_keys: std::collections::BTreeSet<String> = ["created_sketch", "curves", "info", "sketch", "texts"].map(String::from).into();
|
||||
|
||||
// With no sketch being edited: a new one is made, on the plane and with the name given.
|
||||
let r = run(&mut s, "sketch.insert_dxf", json!({"text": DXF, "plane": "XZ", "name": "Plate", "at": [5, 5]}));
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_eq!(keys(&r), insert_keys, "{r}");
|
||||
assert_eq!((r["curves"].as_u64(), r["texts"].as_u64()), (Some(4), Some(0)), "{r}");
|
||||
assert_eq!(r["created_sketch"], r["sketch"], "{r}");
|
||||
// Exactly what the handler returned before it built its reply from the type.
|
||||
let mut handler_reply = r.clone();
|
||||
handler_reply.as_object_mut().unwrap().remove("created");
|
||||
assert_eq!(
|
||||
handler_reply,
|
||||
json!({"sketch": r["sketch"], "created_sketch": r["sketch"], "curves": 4, "texts": 0, "info": {"solved": true, "dof": 8}})
|
||||
);
|
||||
assert!(r["info"]["solved"].is_boolean() && r["info"]["dof"].is_u64(), "{r}");
|
||||
// With one: into it, and `plane` and `name` (of any kind) are not read.
|
||||
let id = s.active_sketch.unwrap();
|
||||
for p in [
|
||||
json!({"text": DXF, "path": 5, "plane": 7, "name": [1]}),
|
||||
json!({"text": DXF, "width": 60, "height": "x", "scale": "y", "angle": 90, "align": "center", "at": {"x": 1, "y": 2}}),
|
||||
json!({"text": DXF, "height": 10, "scale": "y", "tolerance": 0.01}),
|
||||
json!({"text": DXF, "scale": 2, "align": "origin", "sketch": id}),
|
||||
json!({"text": DXF, "sketch": s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default()}),
|
||||
json!({"path": dxf_path, "text": null, "sketch": null}),
|
||||
] {
|
||||
let r = run(&mut s, "sketch.insert_dxf", p);
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_eq!(keys(&r), insert_keys);
|
||||
assert_eq!((r["sketch"].as_u64(), r["created_sketch"].clone(), r["curves"].as_u64()), (Some(id), Value::Null, Some(4)), "{r}");
|
||||
}
|
||||
let r = run(&mut s, "sketch.insert_svg", json!({"text": SVG, "angle": 15}));
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_eq!((r["curves"].as_u64(), r["texts"].as_u64()), (Some(4), Some(1)), "{r}");
|
||||
let r = run(&mut s, "sketch.insert_svg", json!({"path": svg_path, "width": 40, "align": "center", "at": [10, 10]}));
|
||||
assert_eq!(r["texts"], 1, "{r}");
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
let r = run(&mut s, "sketch.insert_svg", json!({"path": svg_path, "plane": "YZ"}));
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
assert_ne!(r["created_sketch"], Value::Null, "{r}");
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
|
||||
// Export: the text, or a file.
|
||||
let r = run(&mut s, "sketch.export_dxf", json!({"sketch": id}));
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
assert_eq!(keys(&r), ["dxf", "entities"].map(String::from).into(), "{r}");
|
||||
assert!(r["dxf"].as_str().is_some_and(|t| t.contains("ENTITIES")), "{r}");
|
||||
assert!(r["entities"].as_u64().is_some_and(|n| n >= 4), "{r}");
|
||||
let out = dir.join("out.dxf");
|
||||
let out_path = out.to_string_lossy().into_owned();
|
||||
let r = run(&mut s, "sketch.export_dxf", json!({"sketch": id, "path": out_path}));
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
assert_eq!(r["path"], json!(out_path), "{r}");
|
||||
assert_eq!(keys(&r), ["entities", "path"].map(String::from).into(), "{r}");
|
||||
assert!(std::fs::read_to_string(&out).unwrap().contains("ENTITIES"));
|
||||
// A blank `path` and a null `sketch` read as left out.
|
||||
s.execute("sketch.edit", &json!({"sketch": id})).unwrap();
|
||||
let r = run(&mut s, "sketch.export_dxf", json!({"sketch": null, "path": " "}));
|
||||
assert!(r.get("dxf").is_some(), "{r}");
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
// The units and layer options, in any case; null and a blank layer read as left out.
|
||||
for p in [
|
||||
json!({"units": "IN", "layer": "PART"}),
|
||||
json!({"units": "design", "layers": "Roles"}),
|
||||
json!({"join": true, "layer": " "}),
|
||||
json!({"units": null, "layer": null, "layers": null, "join": null}),
|
||||
] {
|
||||
let r = run(&mut s, "sketch.export_dxf", p);
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
assert_eq!(keys(&r), ["dxf", "entities"].map(String::from).into(), "{r}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
||||
// A misspelt key runs in warn and says so; the reply still matches. Strict refuses it.
|
||||
let p = json!({"text": DXF, "wdth": 5});
|
||||
let r = s.execute("sketch.insert_dxf", &p).unwrap();
|
||||
assert!(r["param_warnings"].as_array().is_some_and(|w| w.iter().any(|m| m.as_str().is_some_and(|m| m.contains("`wdth`")))), "{r}");
|
||||
matches(output_schema_for::<DrawingInserted>(), &r);
|
||||
let r = s.execute("sketch.export_dxf", &json!({"pth": "a.dxf"})).unwrap();
|
||||
assert!(r["param_warnings"].as_array().is_some_and(|w| !w.is_empty()), "{r}");
|
||||
matches(output_schema_for::<ExportDxfResult>(), &r);
|
||||
let r = s.execute("sketch.insert_svg", &json!({"text": SVG, "angel": 5})).unwrap();
|
||||
assert!(r["param_warnings"].as_array().is_some_and(|w| !w.is_empty()), "{r}");
|
||||
s.param_check = ParamCheck::Strict;
|
||||
let error = s.execute("sketch.insert_dxf", &p).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(error.contains("`wdth`") && error.contains("did you mean `width`"), "{error}");
|
||||
}
|
||||
@@ -67,6 +67,9 @@ sketch.dimension_text
|
||||
sketch.drag
|
||||
sketch.drag_end
|
||||
sketch.draw.dimensioned
|
||||
sketch.export_dxf
|
||||
sketch.insert_dxf
|
||||
sketch.insert_svg
|
||||
sketch.move_point
|
||||
sketch.options
|
||||
sketch.polygon.circumscribed
|
||||
|
||||
@@ -11,6 +11,8 @@ workspace = true
|
||||
[dependencies]
|
||||
solidcore-record = { workspace = true }
|
||||
solidcraft-engine = { workspace = true }
|
||||
# The widget registry (handles, rects, counts) the UI kit publishes into.
|
||||
solidcraft-ui-registry = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
egui-wgpu = { workspace = true }
|
||||
solidcraft-gpu = { workspace = true }
|
||||
|
||||
@@ -189,7 +189,7 @@ pub(crate) fn left_inset(app: &SolveApp, available: Rect) -> f32 {
|
||||
&& app.preferences.agent_chat_side == "left"
|
||||
{
|
||||
let width = if app.preferences.agent_chat_minimized { STRIP } else { app.preferences.agent_chat_width.clamp(MIN_WIDTH, MAX_WIDTH) };
|
||||
(width + crate::shell::SHELL_GAP).min(available.width().max(0.0))
|
||||
(width + crate::ui_kit::SHELL_GAP).min(available.width().max(0.0))
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
@@ -205,9 +205,9 @@ pub(crate) fn tool_space(app: &SolveApp, mut available: Rect) -> Rect {
|
||||
&& let Some(chat) = app.agent_chat.panel_rect.filter(|r| r.is_finite() && r.is_positive())
|
||||
{
|
||||
if app.preferences.agent_chat_side == "left" {
|
||||
available.min.x = crate::shell::within(chat.right() + crate::shell::SHELL_GAP, available.left(), available.right());
|
||||
available.min.x = crate::ui_kit::within(chat.right() + crate::ui_kit::SHELL_GAP, available.left(), available.right());
|
||||
} else {
|
||||
available.max.x = crate::shell::within(chat.left() - crate::shell::SHELL_GAP, available.left(), available.right());
|
||||
available.max.x = crate::ui_kit::within(chat.left() - crate::ui_kit::SHELL_GAP, available.left(), available.right());
|
||||
}
|
||||
}
|
||||
available
|
||||
@@ -216,7 +216,7 @@ pub(crate) fn tool_space(app: &SolveApp, mut available: Rect) -> Rect {
|
||||
/// The same outer rectangle is used for the floating sheet and its docking preview.
|
||||
fn floating_bounds(app: &SolveApp, available: Rect, left: bool) -> Rect {
|
||||
let t = Tokens::get();
|
||||
let gap = crate::shell::SHELL_GAP;
|
||||
let gap = crate::ui_kit::SHELL_GAP;
|
||||
let mut bounds = available.shrink(gap);
|
||||
// The ribbon already supplies the top gap shared by browser and history cards.
|
||||
bounds.min.y = available.top();
|
||||
@@ -1659,7 +1659,7 @@ fn copy_message_menu(ui: &egui::Ui, response: &egui::Response, n: usize, text: &
|
||||
.frame(ui_kit::Menu::frame())
|
||||
.open_memory(if opened { Some(egui::SetOpenCommand::Bool(true)) } else { None })
|
||||
.show(|ui| {
|
||||
ui_kit::Menu::show(ui, crate::context_menu::LIST_W, |ui| {
|
||||
ui_kit::Menu::show(ui, crate::ui_kit::LIST_W, |ui| {
|
||||
let item = ui_kit::MenuItem::new("copy_message", tr("ui.agent_chat_copy_message"), "copy");
|
||||
let row = item.show(ui);
|
||||
crate::scenario::publish_handle(&format!("agent_chat:copy_menu:{n}"), row.rect.center());
|
||||
|
||||
@@ -519,7 +519,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
permissions_section(app, ui);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let browsing = !crate::prefs_search::querying(ui) || app.prefs_window.section == crate::prefs::PAGE_ASSISTANT;
|
||||
let browsing = !crate::ui_kit::settings_search::querying(ui) || app.prefs_window.section == crate::prefs::PAGE_ASSISTANT;
|
||||
if app.preferences.ai_features && browsing && app.agent_chat.detection.is_none() && app.agent_chat.detecting.is_none() {
|
||||
refresh(app, &ui.ctx().clone());
|
||||
}
|
||||
@@ -561,7 +561,7 @@ fn instructions_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let response = ui_kit::text_area(ui, tr("ai.prefs.instructions"), id, text, 6, acp::INSTRUCTIONS_LIMIT);
|
||||
if response.has_focus() {
|
||||
// Typed text belongs to the field, not to Preferences search.
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
crate::ui_kit::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
crate::scenario::publish_target("prefs:agent_instructions", response.rect);
|
||||
ui_kit::text(ui, tr("ai.prefs.instructions_note"), ui_kit::Text::Hint("instructions"));
|
||||
@@ -658,20 +658,20 @@ fn panel_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let previous = usize::from(p.agent_chat_turn_limit != 0);
|
||||
let mut limit = previous;
|
||||
let options = [tr("ui.agent_chat_turn_unlimited"), tr("prefs.assistant_limited")];
|
||||
crate::prefs_search::note_current(ui, &options.join(" "));
|
||||
crate::ui_kit::settings_search::note_current(ui, &options.join(" "));
|
||||
let dropdown_width = ui_kit::ContentWidth::dropdown(ui, &options);
|
||||
let response =
|
||||
ui_kit::settings_control_width(ui, dropdown_width, |ui| ui_kit::dropdown(ui, "assistant_turn_limit", &mut limit, &options));
|
||||
let query = crate::prefs_search::query(ui);
|
||||
let query = crate::ui_kit::settings_search::query(ui);
|
||||
if let Some(option) = options
|
||||
.iter()
|
||||
.find(|option| option.to_lowercase() == query)
|
||||
.or_else(|| options.iter().find(|option| crate::prefs_search::texts_hit(ui, &[*option])))
|
||||
.or_else(|| options.iter().find(|option| crate::ui_kit::settings_search::texts_hit(ui, &[*option])))
|
||||
{
|
||||
crate::prefs_search::remember(ui, option, response.rect);
|
||||
crate::ui_kit::settings_search::remember(ui, option, response.rect);
|
||||
}
|
||||
for option in options {
|
||||
crate::prefs_search::follow(ui, option, response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, option, response.rect);
|
||||
}
|
||||
crate::scenario::publish_target("prefs:agent_chat_turn_limit:mode", response.rect);
|
||||
if limit != previous {
|
||||
@@ -706,7 +706,7 @@ fn pointer_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_form(ui, speed_label_width, |ui| {
|
||||
ui_kit::row(ui, tr("ai.prefs.pointer_speed"), |ui| {
|
||||
let name = tr("prefs.assistant_pointer_speed");
|
||||
crate::prefs_search::note_current(ui, name);
|
||||
crate::ui_kit::settings_search::note_current(ui, name);
|
||||
let response =
|
||||
ui_kit::named_control(ui, name, |ui| crate::agent_cursor::speed_slider(ui, &mut app.ui.agent_cursor.speed));
|
||||
crate::scenario::publish_target("prefs:agent_pointer_speed", response.rect);
|
||||
@@ -717,11 +717,11 @@ fn pointer_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
response.widget_info(|| egui::WidgetInfo::slider(ui.is_enabled(), value, name));
|
||||
// The visible row owns Speed and its legacy Motion speed alias. The
|
||||
// native editor's name owns queries that only match Pointer speed.
|
||||
if !crate::prefs_search::texts_hit(ui, &[tr("ai.prefs.pointer_speed")]) {
|
||||
crate::prefs_search::remember(ui, name, response.rect);
|
||||
if !crate::ui_kit::settings_search::texts_hit(ui, &[tr("ai.prefs.pointer_speed")]) {
|
||||
crate::ui_kit::settings_search::remember(ui, name, response.rect);
|
||||
}
|
||||
for label in [tr("ai.prefs.pointer_speed"), name] {
|
||||
crate::prefs_search::follow(ui, label, response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, label, response.rect);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -781,12 +781,12 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
for entry in &all {
|
||||
ui.push_id(&entry.id, |ui| {
|
||||
let detail = entry.detail();
|
||||
let gate = crate::prefs_search::block_gate(ui, &entry.name, Some(&format!("{} {detail}", entry.id)));
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let gate = crate::ui_kit::settings_search::block_gate(ui, &entry.name, Some(&format!("{} {detail}", entry.id)));
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
let mut paint = |ui: &mut egui::Ui| {
|
||||
let card = ui_kit::settings_card(ui, |ui| {
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
crate::prefs_search::visible(ui, &[&entry.name, &entry.id, &detail]);
|
||||
crate::ui_kit::settings_search::visible(ui, &[&entry.name, &entry.id, &detail]);
|
||||
ui.label(egui::RichText::new(&entry.name).strong().size(t.font_label).color(t.form_ink));
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(ui.available_width(), t.control_height),
|
||||
@@ -851,7 +851,7 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let shown = current.map_or_else(|| tr("ui.ai_mode_global").to_owned(), |mode| mode.label().to_owned());
|
||||
let mut pick: Option<Option<Mode>> = None;
|
||||
let options: Vec<_> = std::iter::once(tr("ui.ai_mode_global")).chain(Mode::ALL.map(Mode::label)).collect();
|
||||
crate::prefs_search::note_current(ui, &options.join(" "));
|
||||
crate::ui_kit::settings_search::note_current(ui, &options.join(" "));
|
||||
let response = ui_kit::settings_control_width(ui, t.space_24 * 9.0 + t.space_4, |ui| {
|
||||
ui_kit::dropdown_menu(ui, ("sc_ai_mode", &entry.id), &shown, |ui| {
|
||||
if ui_kit::list_picker(ui, "global", tr("ui.ai_mode_global"), current.is_none()).clicked() {
|
||||
@@ -864,16 +864,16 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
})
|
||||
});
|
||||
let query = crate::prefs_search::query(ui);
|
||||
let query = crate::ui_kit::settings_search::query(ui);
|
||||
if let Some(option) = options
|
||||
.iter()
|
||||
.find(|option| option.to_lowercase() == query)
|
||||
.or_else(|| options.iter().find(|option| crate::prefs_search::texts_hit(ui, &[*option])))
|
||||
.or_else(|| options.iter().find(|option| crate::ui_kit::settings_search::texts_hit(ui, &[*option])))
|
||||
{
|
||||
crate::prefs_search::remember(ui, option, response.rect);
|
||||
crate::ui_kit::settings_search::remember(ui, option, response.rect);
|
||||
}
|
||||
for option in options {
|
||||
crate::prefs_search::follow(ui, option, response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, option, response.rect);
|
||||
}
|
||||
match pick {
|
||||
Some(Some(mode)) => {
|
||||
@@ -965,23 +965,23 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
crate::scenario::publish_rect(&format!("prefs:ai_card:{}", entry.id), card.response.rect);
|
||||
let metadata = [&entry.name, &entry.id, &detail];
|
||||
if let Some(text) = metadata.iter().find(|text| crate::prefs_search::texts_hit(ui, &[*text])) {
|
||||
crate::prefs_search::remember(ui, text, card.response.rect);
|
||||
if let Some(text) = metadata.iter().find(|text| crate::ui_kit::settings_search::texts_hit(ui, &[*text])) {
|
||||
crate::ui_kit::settings_search::remember(ui, text, card.response.rect);
|
||||
}
|
||||
for text in metadata {
|
||||
crate::prefs_search::follow(ui, text, card.response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, text, card.response.rect);
|
||||
}
|
||||
};
|
||||
let show = gate.as_ref().is_none_or(|gate| gate.show);
|
||||
if show {
|
||||
paint(ui);
|
||||
} else {
|
||||
crate::prefs_search::run_hidden(ui, &entry.id, |ui| {
|
||||
crate::ui_kit::settings_search::run_hidden(ui, &entry.id, |ui| {
|
||||
paint(ui);
|
||||
});
|
||||
}
|
||||
if let Some(gate) = gate {
|
||||
crate::prefs_search::block_finish(ui, &gate, show, before);
|
||||
crate::ui_kit::settings_search::block_finish(ui, &gate, show, before);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1524,7 +1524,7 @@ mod card_tests {
|
||||
|
||||
#[test]
|
||||
fn pointer_speed_search_reveals_one_real_slider_in_all_enablement_states() {
|
||||
use crate::prefs_search::{nav, set_cursor};
|
||||
use crate::ui_kit::settings_search::{nav, set_cursor};
|
||||
for theme in ["light", "dark"] {
|
||||
for (ai, show) in [(true, true), (true, false), (false, true), (false, false)] {
|
||||
let mut h = Harness::new();
|
||||
@@ -1569,7 +1569,7 @@ mod card_tests {
|
||||
|
||||
#[test]
|
||||
fn overlapping_agent_metadata_has_one_card_target_and_keeps_fields_indexed() {
|
||||
use crate::prefs_search::{nav, set_cursor};
|
||||
use crate::ui_kit::settings_search::{nav, set_cursor};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
fn transcript_clipboard_harness(layout: crate::shell::LayoutMode) -> crate::scenario::Harness {
|
||||
fn transcript_clipboard_harness(layout: crate::ui_kit::LayoutMode) -> crate::scenario::Harness {
|
||||
let mut h = narrow_chat();
|
||||
h.app.ui.layout = layout;
|
||||
h.app.preferences.agent_chat_width = 500.0;
|
||||
@@ -36,7 +36,7 @@ fn transcript_click(h: &mut crate::scenario::Harness, pos: egui::Pos2) {
|
||||
|
||||
#[test]
|
||||
fn transcript_copy_and_cut_do_not_touch_the_cad_clipboard() {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = transcript_clipboard_harness(layout);
|
||||
let at = transcript_text_position(&h);
|
||||
transcript_click(&mut h, at);
|
||||
@@ -57,7 +57,7 @@ fn transcript_copy_and_cut_do_not_touch_the_cad_clipboard() {
|
||||
|
||||
#[test]
|
||||
fn transcript_paste_does_not_insert_a_previously_copied_body() {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = transcript_clipboard_harness(layout);
|
||||
h.frame_with(vec![egui::Event::Copy]);
|
||||
assert!(!h.app.copied_bodies.is_empty(), "prime the real CAD clipboard");
|
||||
@@ -77,7 +77,7 @@ fn transcript_paste_does_not_insert_a_previously_copied_body() {
|
||||
|
||||
#[test]
|
||||
fn transcript_key_events_do_not_run_cad_shortcuts() {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = transcript_clipboard_harness(layout);
|
||||
let at = transcript_text_position(&h);
|
||||
transcript_click(&mut h, at);
|
||||
@@ -106,7 +106,7 @@ fn transcript_key_events_do_not_run_cad_shortcuts() {
|
||||
#[test]
|
||||
fn transcript_shortcut_ownership_ends_when_the_panel_is_hidden() {
|
||||
for hidden in ["closed", "minimized", "disabled"] {
|
||||
let mut h = transcript_clipboard_harness(crate::shell::LayoutMode::Floating);
|
||||
let mut h = transcript_clipboard_harness(crate::ui_kit::LayoutMode::Floating);
|
||||
h.frame_with(vec![egui::Event::Copy]);
|
||||
let at = transcript_text_position(&h);
|
||||
transcript_click(&mut h, at);
|
||||
@@ -3184,7 +3184,7 @@ fn a_press_at_the_edge_without_a_drag_towards_it_scrolls_nothing() {
|
||||
/// at the panel's edge or is cut off at the other one.
|
||||
#[test]
|
||||
fn the_agent_list_keeps_the_panels_side_margin() {
|
||||
use crate::shell::LayoutMode;
|
||||
use crate::ui_kit::LayoutMode;
|
||||
for layout in [LayoutMode::Docked, LayoutMode::Floating] {
|
||||
let mut h = narrow_chat();
|
||||
h.app.ui.layout = layout;
|
||||
@@ -4634,7 +4634,7 @@ fn the_composer_hint_stays_on_one_row_at_the_narrowest_width() {
|
||||
#[test]
|
||||
fn tool_room_beside_the_docked_assistant_survives_an_inside_out_view() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
app.ui.layout = crate::shell::LayoutMode::Floating;
|
||||
app.ui.layout = crate::ui_kit::LayoutMode::Floating;
|
||||
app.preferences.ai_features = true;
|
||||
app.agent_chat.open = true;
|
||||
app.preferences.agent_chat_popped = false;
|
||||
|
||||
@@ -148,8 +148,9 @@ use serde_json::{Value, json};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::{BrowserEntry, Document, FeatureKind};
|
||||
|
||||
use crate::context_menu::{Item, Target};
|
||||
use crate::context_menu::Target;
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::Item;
|
||||
|
||||
use crate::SolveApp;
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ use solidcraft_engine::view::POINT_CLOUDS_FOLDER;
|
||||
|
||||
use super::{Actions, Row, draw_row, is_open, panel_pos, rows_start_open, toggle, toggle_folder};
|
||||
use crate::SolveApp;
|
||||
use crate::context_menu::{Item, Target};
|
||||
use crate::context_menu::Target;
|
||||
use crate::ui_kit;
|
||||
use crate::ui_kit::Item;
|
||||
|
||||
#[path = "point_cloud_tracks.rs"]
|
||||
pub(crate) mod point_cloud_tracks;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use crate::scenario::{Harness, handle_at, rect_of};
|
||||
use crate::semantic_content::{self, ContentOwner, ContentRequest};
|
||||
use crate::shell::{LayoutMode, SHELL_GAP};
|
||||
use crate::ui_kit::{LayoutMode, SHELL_GAP};
|
||||
|
||||
#[test]
|
||||
fn clicked_browser_folders_toggle_visibility_with_v() {
|
||||
@@ -318,7 +318,7 @@ fn selected_row_paints_tab_soft_fill_and_accent_text_in_both_layouts() {
|
||||
let mut app = SolveApp::new(Default::default(), Default::default());
|
||||
app.ui.layout = mode;
|
||||
app.ui.tab = tab.into();
|
||||
crate::theme::TabAccent::publish(&app, &ctx);
|
||||
app.publish_tab_accent(&ctx);
|
||||
let accent = crate::theme::TabAccent::for_tab(&Tokens::get(), tab);
|
||||
let mut row_rect = Rect::NOTHING;
|
||||
let out = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
|
||||
@@ -646,7 +646,7 @@ fn browser_width_rejects_hostile_preferences_and_tiny_windows_still_fit() {
|
||||
assert_eq!(browser_width(-100.0), MIN_BROWSER_WIDTH);
|
||||
assert_eq!(browser_width(f32::MAX), MAX_BROWSER_WIDTH);
|
||||
let available = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 300.0));
|
||||
for top_gap in [0.0, crate::shell::SHELL_GAP] {
|
||||
for top_gap in [0.0, crate::ui_kit::SHELL_GAP] {
|
||||
let bounds = panel_bounds(available, None, MAX_BROWSER_WIDTH, top_gap);
|
||||
assert!(available.contains_rect(bounds));
|
||||
}
|
||||
@@ -677,7 +677,7 @@ fn collapsed_rail_shares_header_hover_inset_and_expands_from_its_whole_card() {
|
||||
app.ui.tab = "SURFACE".into();
|
||||
app.preferences.timeline_left = true;
|
||||
app.preferences.timeline_left_browser_collapsed = collapsed;
|
||||
crate::theme::TabAccent::publish(&app, &ctx);
|
||||
app.publish_tab_accent(&ctx);
|
||||
let bounds = Rect::from_min_size(Pos2::ZERO, vec2(600.0, 700.0));
|
||||
let draw = |app: &mut SolveApp, events| {
|
||||
crate::test_support::frame(&ctx, egui::RawInput { screen_rect: Some(bounds), events, ..Default::default() }, |ui| {
|
||||
|
||||
@@ -211,11 +211,7 @@ fn with_rows_starting_closed_a_designs_folders_open_with_their_arrow_or_find_in_
|
||||
"the Bodies folder stays closed"
|
||||
);
|
||||
|
||||
crate::context_menu::run_item(
|
||||
&mut h.app,
|
||||
&crate::context_menu::Item::action("ui.findBrowser", "", "").with(json!({"body":"Block"})),
|
||||
egui::Pos2::ZERO,
|
||||
);
|
||||
crate::context_menu::run_item(&mut h.app, &crate::ui_kit::Item::action("ui.findBrowser", "", "").with(json!({"body":"Block"})), egui::Pos2::ZERO);
|
||||
h.frames(2);
|
||||
assert!(super::is_open(&h.app, "c0/bodies", false), "Find in Browser opens the Bodies folder");
|
||||
assert!(
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! state; [`items`] lists them and [`run_item`] runs one, so the control channel (`ui.menu`,
|
||||
//! `ui.menuPick`) and tests reach everything a click does.
|
||||
|
||||
use crate::ui_kit::{BackingTooltipExt as _, Container, Surface};
|
||||
use crate::ui_kit::{BackingTooltipExt as _, Container, Item, LIST_W, Surface};
|
||||
use egui::{Color32, Pos2, Rect, Sense, Vec2, pos2, vec2};
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -127,88 +127,6 @@ pub enum Target {
|
||||
},
|
||||
}
|
||||
|
||||
/// One menu entry: a command id (run with `params` when given, else started like a toolbar
|
||||
/// click) or a `ui.*` action. `-` is a separator, `#…` a section heading.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub shortcut: String,
|
||||
pub enabled: bool,
|
||||
pub icon: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub params: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub children: Vec<Item>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub swatch: Option<[u8; 3]>,
|
||||
/// Why the entry is off: shown on hover, and in the control channel's report.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tip: Option<String>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// A view action (`ui.*`).
|
||||
pub fn action(id: &str, label: &str, icon: &str) -> Item {
|
||||
act(id, label, icon)
|
||||
}
|
||||
pub fn sep() -> Item {
|
||||
Item {
|
||||
id: "-".into(),
|
||||
label: String::new(),
|
||||
shortcut: String::new(),
|
||||
enabled: false,
|
||||
icon: String::new(),
|
||||
params: None,
|
||||
children: Vec::new(),
|
||||
swatch: None,
|
||||
tip: None,
|
||||
}
|
||||
}
|
||||
pub(crate) fn heading(label: &str) -> Item {
|
||||
Item {
|
||||
id: format!("#{label}"),
|
||||
label: label.into(),
|
||||
shortcut: String::new(),
|
||||
enabled: false,
|
||||
icon: String::new(),
|
||||
params: None,
|
||||
children: Vec::new(),
|
||||
swatch: None,
|
||||
tip: None,
|
||||
}
|
||||
}
|
||||
fn is_sep(&self) -> bool {
|
||||
self.id == "-"
|
||||
}
|
||||
fn is_heading(&self) -> bool {
|
||||
self.id.starts_with('#')
|
||||
}
|
||||
pub fn with(mut self, params: Value) -> Item {
|
||||
self.params = Some(params);
|
||||
self
|
||||
}
|
||||
pub fn on_if(self, enabled: bool) -> Item {
|
||||
self.on(enabled)
|
||||
}
|
||||
pub(crate) fn on(mut self, enabled: bool) -> Item {
|
||||
self.enabled &= enabled;
|
||||
self
|
||||
}
|
||||
/// Off, saying why, when there is a reason.
|
||||
pub(crate) fn off_because(mut self, why: Option<String>) -> Item {
|
||||
if let Some(why) = why {
|
||||
self.enabled = false;
|
||||
self.tip = Some(why);
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn key(mut self, k: &str) -> Item {
|
||||
self.shortcut = k.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A command entry: label, icon, shortcut and enablement come from the registry.
|
||||
pub(crate) fn cmd(app: &SolveApp, id: &str, label: &str) -> Item {
|
||||
let info = solidcraft_engine::find_command(id).map(|c| c.info(&app.session));
|
||||
@@ -232,17 +150,7 @@ pub(crate) fn cmd(app: &SolveApp, id: &str, label: &str) -> Item {
|
||||
|
||||
/// A view action (`ui.*`).
|
||||
pub(crate) fn act(id: &str, label: &str, icon: &str) -> Item {
|
||||
Item {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
shortcut: String::new(),
|
||||
enabled: true,
|
||||
icon: icon.into(),
|
||||
params: None,
|
||||
children: Vec::new(),
|
||||
swatch: None,
|
||||
tip: None,
|
||||
}
|
||||
Item::action(id, label, icon)
|
||||
}
|
||||
|
||||
/// Menu state kept between frames. The anchor is `app.viewport.context_menu`.
|
||||
@@ -2535,9 +2443,6 @@ fn list_place(c: Pos2, h: f32, screen: Rect) -> Pos2 {
|
||||
pos2((c.x - RX - 12.0 - LIST_W).max(screen.left() + 4.0), y)
|
||||
}
|
||||
|
||||
/// Width of list menus.
|
||||
pub const LIST_W: f32 = 250.0;
|
||||
|
||||
/// Where a menu of `size` opened at `at` goes: right of and below the point (`gap` away) when it
|
||||
/// fits in `screen`, flipped to the left or above it when it would run past that edge, and
|
||||
/// against the far edge when it fits on neither side. A flipped menu ends at the cursor, so it
|
||||
|
||||
@@ -790,7 +790,7 @@ fn revolve_layout_fixture_builds_a_valid_solid() {
|
||||
|
||||
#[test]
|
||||
fn new_cards_settle_before_the_first_click_in_both_layouts() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for command in ["solid.box", "solid.revolve", "solid.shell", "joint.create"] {
|
||||
let mut h = fixture();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
|
||||
@@ -1202,7 +1202,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn common_forms_fit_both_layouts_and_scales_and_pin_footer_at_small_heights() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for scale in [1.0, 2.0] {
|
||||
for height in [950.0, 720.0] {
|
||||
let mut h = Harness::new();
|
||||
@@ -1244,7 +1244,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tiny_windows_do_not_panic_or_publish_non_finite_cards() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for size in [(1.0, 1.0), (40.0, 80.0), (180.0, 240.0)] {
|
||||
let mut h = Harness::new();
|
||||
h.call("ui.set", json!({"layout": mode}));
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::Services;
|
||||
use crate::dialogs::{apply_commands, for_feature as edit_dialog};
|
||||
|
||||
/// A 100 x 60 base flange on XY (2.5 mm steel).
|
||||
fn plate(mode: crate::shell::LayoutMode) -> SolveApp {
|
||||
fn plate(mode: crate::ui_kit::LayoutMode) -> SolveApp {
|
||||
let mut s = Session::default();
|
||||
s.execute("sketch.create", &json!({"plane": "XY", "name": "Base"})).unwrap();
|
||||
s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [100, 60]})).unwrap();
|
||||
@@ -21,7 +21,7 @@ fn plate(mode: crate::shell::LayoutMode) -> SolveApp {
|
||||
/// dialog's height, angle and bend position; Edit Feature brings the edges back.
|
||||
#[test]
|
||||
fn flange_follows_the_picks() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
let sk = app.session.doc.features[0].id;
|
||||
app.session.selection = vec![Sel::Profile { sketch: sk, index: 0 }];
|
||||
@@ -95,7 +95,7 @@ fn a_click_on_the_bottom_edge_makes_an_edge_flange() {
|
||||
/// Hem takes edges and an optional gap; rules make and pick a rule with its expressions.
|
||||
#[test]
|
||||
fn hem_and_rules() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.session.selection = edge_sel(&app.session, Vec3::new(50.0, 0.0, 2.5)).into_iter().collect();
|
||||
@@ -133,7 +133,7 @@ fn hem_and_rules() {
|
||||
|
||||
#[test]
|
||||
fn flat_exports_use_the_save_picker() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
use std::sync::{Arc, Mutex};
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
@@ -157,7 +157,7 @@ fn flat_exports_use_the_save_picker() {
|
||||
|
||||
#[test]
|
||||
fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.run("sketch.create", json!({"plane": "XY", "name": "Pocket"})).unwrap();
|
||||
@@ -225,7 +225,7 @@ fn flat_pattern_shows_each_side_of_the_flat_size_whole() {
|
||||
|
||||
#[test]
|
||||
fn flat_pattern_islands_fit_before_the_actual_command_panel() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.run("sketch.create", json!({"plane": "XY", "name": "Retain"})).unwrap();
|
||||
@@ -247,7 +247,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
|
||||
let viewport = Rect::from_min_max(pos2(250.0, crate::toolbar::APP_BAR_HEIGHT + crate::toolbar::RIBBON_HEIGHT), pos2(1400.0, 832.0));
|
||||
let browser = Rect::from_min_max(viewport.min, viewport.min + vec2(250.0, 450.0));
|
||||
let history = Rect::from_min_max(pos2(viewport.left(), viewport.bottom() - 40.0), viewport.max);
|
||||
if mode == crate::shell::LayoutMode::Floating {
|
||||
if mode == crate::ui_kit::LayoutMode::Floating {
|
||||
app.shell.content = Some(Rect::from_min_max(pos2(browser.right(), viewport.top()), pos2(viewport.right(), history.top())));
|
||||
}
|
||||
let ctx = crate::fonts::test_context();
|
||||
@@ -286,7 +286,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
|
||||
outlines.iter().flat_map(|p| &p.points).all(|p| p.x <= panel.left() - 19.0),
|
||||
"both complete islands must remain exposed before the command panel: {panel:?}"
|
||||
);
|
||||
if mode == crate::shell::LayoutMode::Floating {
|
||||
if mode == crate::ui_kit::LayoutMode::Floating {
|
||||
assert!(
|
||||
outlines.iter().flat_map(|p| &p.points).all(|p| !browser.contains(*p) && !history.contains(*p)),
|
||||
"flat pattern fits clear of the shell cards"
|
||||
@@ -297,7 +297,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
|
||||
|
||||
#[test]
|
||||
fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch":"Base"})).unwrap();
|
||||
app.run("sketch.create", json!({"plane":"XY","name":"Retain"})).unwrap();
|
||||
@@ -329,7 +329,7 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
|
||||
|
||||
#[test]
|
||||
fn pocket_export_button_uses_the_picker_and_preserves_depth_layers() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
for (command, params) in [
|
||||
@@ -366,7 +366,7 @@ fn pocket_export_button_uses_the_picker_and_preserves_depth_layers() {
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn bend_table_picker_writes_the_current_body() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.run("sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20})).unwrap();
|
||||
@@ -394,7 +394,7 @@ fn bend_table_picker_writes_the_current_body() {
|
||||
/// made with it, and the dialog shows it the next time.
|
||||
#[test]
|
||||
fn rules_dialog_picks_the_corner_relief_shape() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
h.app
|
||||
@@ -447,7 +447,7 @@ fn rules_dialog_picks_the_corner_relief_shape() {
|
||||
/// of the part made with it, and the dialog shows it the next time.
|
||||
#[test]
|
||||
fn rules_dialog_sets_how_bend_angles_read() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
h.app.run("sheet.manage_rules", json!({"name": "AL2", "thickness": "2 mm", "bend_radius": "1 mm", "active": true})).unwrap();
|
||||
@@ -551,7 +551,7 @@ fn the_rule_manager_relieves_a_corner_that_is_not_square() {
|
||||
/// a shape and a size reach the feature, Edit Feature shows them again, and Rule takes them back.
|
||||
#[test]
|
||||
fn flange_dialog_picks_a_corner_relief_of_its_own() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
h.app
|
||||
@@ -627,7 +627,7 @@ fn flange_dialog_picks_a_corner_relief_of_its_own() {
|
||||
|
||||
#[test]
|
||||
fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("document.units", json!({"units":"in"})).unwrap();
|
||||
assert_eq!(super::pocket_label(&app, &json!({"side":"top","depth_mm":25.4*0.1234})).as_deref(), Some("Top pocket: 0.1234 in deep"));
|
||||
@@ -640,7 +640,7 @@ fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
|
||||
/// brings them back and rebuilds the same flanges.
|
||||
#[test]
|
||||
fn edge_flanges_can_have_an_angle_and_a_height_of_their_own() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let edges: Vec<Sel> = [[50.0, 0.0, 2.5], [50.0, 60.0, 2.5], [0.0, 30.0, 2.5], [100.0, 30.0, 2.5]]
|
||||
@@ -703,7 +703,7 @@ fn edge_flanges_can_have_an_angle_and_a_height_of_their_own() {
|
||||
/// one flange command, one body and one undo step, and a bend with no height is refused.
|
||||
#[test]
|
||||
fn further_bends_in_the_flange_dialog_make_one_chained_flap() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.run("sheet.flange", json!({"edges": [[0, 30, 2.5]], "height": 30})).unwrap();
|
||||
@@ -777,7 +777,7 @@ fn add_bend_and_remove_bend_buttons_work() {
|
||||
/// reaches the feature, and Edit Feature shows it again.
|
||||
#[test]
|
||||
fn flange_dialog_measures_the_height_from_the_chosen_datum() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
h.app.run("sketch.create", json!({"plane": "XY", "name": "Base"})).unwrap();
|
||||
@@ -901,7 +901,7 @@ fn convert_takes_the_fixed_face_and_the_bend_edges() {
|
||||
/// and Edit Feature brings the edge and the path back.
|
||||
#[test]
|
||||
fn path_flange_dialog_follows_the_path() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.run("sheet.flange", json!({"edges": [[0, 30, 2.5]], "height": 30})).unwrap();
|
||||
@@ -949,7 +949,7 @@ fn path_flange_dialog_follows_the_path() {
|
||||
|
||||
// ---- flat pattern navigation ----
|
||||
|
||||
fn flat_app(preset: &str, mode: crate::shell::LayoutMode) -> SolveApp {
|
||||
fn flat_app(preset: &str, mode: crate::ui_kit::LayoutMode) -> SolveApp {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap();
|
||||
@@ -989,7 +989,7 @@ fn frame(app: &mut SolveApp, ctx: &egui::Context, time: &mut f64, mods: egui::Mo
|
||||
/// cursor. The flat view never orbits.
|
||||
#[test]
|
||||
fn flat_pattern_pans_and_zooms_like_the_design_viewport() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
let none = Modifiers::NONE;
|
||||
let chords = [none, Modifiers::SHIFT, Modifiers::CTRL, Modifiers::ALT, Modifiers::SHIFT | Modifiers::ALT];
|
||||
@@ -1094,7 +1094,7 @@ impl IntoNav for egui::PointerButton {
|
||||
/// camera and selection alone.
|
||||
#[test]
|
||||
fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
let none = Modifiers::NONE;
|
||||
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
|
||||
@@ -1136,7 +1136,7 @@ fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() {
|
||||
/// Fit and Home frame the flat pattern (not the model), and leave the Design camera alone.
|
||||
#[test]
|
||||
fn fit_and_home_frame_the_flat_pattern() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let mut app = flat_app("fusion", mode);
|
||||
let mut t = 0.0;
|
||||
@@ -1172,7 +1172,7 @@ fn fit_and_home_frame_the_flat_pattern() {
|
||||
}
|
||||
|
||||
fn perforated_flat() -> (SolveApp, Dialog) {
|
||||
let mut app = plate(crate::shell::LayoutMode::Floating);
|
||||
let mut app = plate(crate::ui_kit::LayoutMode::Floating);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
app.run("sheet.perforate", json!({"diameter": 3, "pitch": 6})).unwrap();
|
||||
let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap();
|
||||
@@ -1251,7 +1251,7 @@ fn flat_perforations_cull_offscreen_and_subpixel_holes() {
|
||||
#[test]
|
||||
fn hem_handle_runs_along_the_hem_to_its_tip() {
|
||||
for flipped in [false, true] {
|
||||
let mut app = plate(crate::shell::LayoutMode::Floating);
|
||||
let mut app = plate(crate::ui_kit::LayoutMode::Floating);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let body = app.session.model.state().sheets[0].body.clone();
|
||||
app.session.selection = vec![edge_sel(&app.session, Vec3::new(50.0, 0.0, 2.5)).unwrap()];
|
||||
@@ -1296,7 +1296,7 @@ fn inch_sheet_rule_edit_preserves_stored_lengths_and_dimensionless_k_factor() {
|
||||
|
||||
#[test]
|
||||
fn perforate_toolbar_opens_without_running_empty_parameters() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut app = plate(mode);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let features = app.session.doc.features.len();
|
||||
@@ -1328,7 +1328,7 @@ fn perforate_toolbar_opens_without_running_empty_parameters() {
|
||||
#[test]
|
||||
fn perforate_ok_targets_the_picked_sheet_and_updates_flat_data() {
|
||||
for face in [false, true] {
|
||||
let mut app = plate(crate::shell::LayoutMode::Docked);
|
||||
let mut app = plate(crate::ui_kit::LayoutMode::Docked);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let body = app.session.model.state().sheets[0].body.clone();
|
||||
app.session.selection =
|
||||
@@ -1349,7 +1349,7 @@ fn perforate_ok_targets_the_picked_sheet_and_updates_flat_data() {
|
||||
|
||||
#[test]
|
||||
fn perforate_edit_restores_dimensions_pattern_and_target() {
|
||||
let mut app = plate(crate::shell::LayoutMode::Docked);
|
||||
let mut app = plate(crate::ui_kit::LayoutMode::Docked);
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let body = app.session.model.state().sheets[0].body.clone();
|
||||
app.run("sheet.perforate", json!({"body": body, "diameter": "4 mm", "pitch": "9 mm", "margin": "5 mm", "pattern": "square"})).unwrap();
|
||||
@@ -1366,7 +1366,7 @@ fn perforate_edit_restores_dimensions_pattern_and_target() {
|
||||
#[test]
|
||||
fn flat_pattern_navigation_slots_hide_orbit_controls_and_keep_fit_home() {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let mut app = flat_app("fusion", crate::shell::LayoutMode::Floating);
|
||||
let mut app = flat_app("fusion", crate::ui_kit::LayoutMode::Floating);
|
||||
app.preferences.navigation_bar = serde_json::from_value(json!({
|
||||
"slots":["orbit","spin","look_at","projection","visual_style","pan","zoom","fit","home","display"]
|
||||
}))
|
||||
|
||||
@@ -753,7 +753,7 @@ fn label_marked(k: &ConstraintKind, radius_mark: bool, expr: Option<&str>, drive
|
||||
let plain = expr.is_none_or(|e| {
|
||||
let e = e.trim();
|
||||
let e = ["mm", "cm", "in", "ft", "m", "deg", "rad", "°"].iter().find_map(|unit| e.strip_suffix(unit)).unwrap_or(e);
|
||||
crate::prefs::parse_number(e).is_some()
|
||||
crate::ui_kit::parse_number(e).is_some()
|
||||
});
|
||||
let text = if plain { num } else { format!("fx: {num}") };
|
||||
if driven { format!("({text})") } else { text }
|
||||
|
||||
@@ -1188,7 +1188,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
|
||||
crate::context_menu::menu_frame(ui, |ui| {
|
||||
let mut chosen = None;
|
||||
for (id, label) in items {
|
||||
let mut item = crate::context_menu::Item::action(id, label, "");
|
||||
let mut item = crate::ui_kit::Item::action(id, label, "");
|
||||
if id == "ui.closeTab" && i == app.docs.active {
|
||||
item = item.key("Ctrl+W");
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use syn::{Expr, Item, Lit};
|
||||
const ALLOWED_LITERALS: &[(&str, usize, &str)] = &[
|
||||
("dialogs.rs", 2, "preview spinners: graphic diameter, not typography"),
|
||||
("viewport.rs", 1, "viewport status spinner: graphic diameter, not typography"),
|
||||
("ui_kit/settings_search.rs", 2, "hidden off-screen areas that measure search matches: never drawn"),
|
||||
];
|
||||
|
||||
fn test_only(attrs: &[syn::Attribute]) -> bool {
|
||||
|
||||
@@ -101,7 +101,7 @@ pub fn drag_dock_named(ui: &Ui, handle: &str, band: egui::Rect, key: &str, moved
|
||||
mod tests {
|
||||
#[test]
|
||||
fn feature_forms_use_available_height_and_keep_actions_visible() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
use crate::scenario::Harness;
|
||||
use serde_json::json;
|
||||
for (width, height, scale) in [(1400.0, 900.0, 1.0), (1400.0, 950.0, 2.0)] {
|
||||
@@ -167,7 +167,7 @@ mod tests {
|
||||
use crate::scenario::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.call("ui.set", json!({"layout": mode}));
|
||||
h.call("ui.resize", json!({"width": 2400, "height": 900}));
|
||||
@@ -214,7 +214,7 @@ mod tests {
|
||||
/// drags it; reopening keeps its position.
|
||||
#[test]
|
||||
fn a_docked_dialog_keeps_its_size_under_the_pointer_and_moves_by_its_title() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
use crate::scenario::Harness;
|
||||
use serde_json::json;
|
||||
let mut h = Harness::new();
|
||||
@@ -254,7 +254,7 @@ mod tests {
|
||||
fn command_dialogs_keep_the_dragged_position_after_closing() {
|
||||
use crate::scenario::Harness;
|
||||
use serde_json::json;
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
h.call("ui.resize", json!({"width":1400.0,"height":900.0}));
|
||||
|
||||
@@ -147,14 +147,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
|
||||
.into_iter()
|
||||
.filter(|(id, ..)| *id != "install_cli" || cfg!(target_os = "macos"))
|
||||
{
|
||||
let item = crate::context_menu::Item::action(id, label, icon).on_if(available(app, id));
|
||||
let item = crate::ui_kit::Item::action(id, label, icon).on_if(available(app, id));
|
||||
if crate::context_menu::list_row(ui, &item).is_some() {
|
||||
pick = Some(id);
|
||||
}
|
||||
// A layout without the version in the application bar lists it here.
|
||||
if id == "about" && app.header.version == crate::toolbar::VersionPlace::HelpMenu {
|
||||
let label = crate::i18n::format("help.version", &[("version", crate::update::current_version())]);
|
||||
if crate::context_menu::list_row(ui, &crate::context_menu::Item::action("version", &label, "update")).is_some() {
|
||||
if crate::context_menu::list_row(ui, &crate::ui_kit::Item::action("version", &label, "update")).is_some() {
|
||||
pick = Some("version");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ use crate::test_support::sample_app;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::browser;
|
||||
use crate::context_menu::{self, Item, Target};
|
||||
use crate::context_menu::{self, Target};
|
||||
use crate::ui_kit::Item;
|
||||
use crate::{Services, SolveApp};
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::Session;
|
||||
|
||||
@@ -28,20 +28,6 @@ const MAX_URL: usize = 6000;
|
||||
/// past about 7,600 characters GitHub drops it and the form comes back empty after signing in.
|
||||
const MAX_SIGN_IN_URL: usize = 7000;
|
||||
|
||||
thread_local! {
|
||||
static CAD_INPUT_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Tests set the barrier by hand to see what ignores it.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_cad_input_blocked_for_test(blocked: bool) {
|
||||
CAD_INPUT_BLOCKED.with(|cell| cell.set(blocked));
|
||||
}
|
||||
|
||||
pub(crate) fn cad_input_blocked() -> bool {
|
||||
CAD_INPUT_BLOCKED.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// Draw CAD behind a report or native picker without delivering input or focus to editors.
|
||||
/// Fresh focus history also prevents inline editors from committing on report focus loss.
|
||||
pub(crate) struct CadInput {
|
||||
@@ -85,14 +71,15 @@ impl CadInput {
|
||||
*m = neutral;
|
||||
saved
|
||||
});
|
||||
let previous_blocked = CAD_INPUT_BLOCKED.with(|blocked| blocked.replace(true));
|
||||
let previous_blocked = crate::ui_kit::input_blocked();
|
||||
crate::ui_kit::set_input_blocked(true);
|
||||
Some(Self { ctx: ctx.clone(), input: Some(input), memory: Some(memory), previous_blocked })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CadInput {
|
||||
fn drop(&mut self) {
|
||||
CAD_INPUT_BLOCKED.with(|blocked| blocked.set(self.previous_blocked));
|
||||
crate::ui_kit::set_input_blocked(self.previous_blocked);
|
||||
if let Some(input) = self.input.take() {
|
||||
self.ctx.input_mut(|i| *i = input);
|
||||
}
|
||||
@@ -1836,7 +1823,7 @@ mod tests {
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
app.issue_report.draft = None;
|
||||
assert!(!cad_input_blocked());
|
||||
assert!(!crate::ui_kit::input_blocked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -578,7 +578,7 @@ pub fn reset(app: &mut SolveApp, id: Option<&str>) {
|
||||
/// reset; a key another command uses is reported and taken over only on request.
|
||||
pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
let filtering = crate::prefs_search::querying(ui);
|
||||
let filtering = crate::ui_kit::settings_search::querying(ui);
|
||||
// A Preferences search lists the matching shortcuts too, and one recorded from there takes
|
||||
// the key like one on this page (#10043). Only a search field that has the keyboard keeps
|
||||
// its keys: going back to it to type stops the recording.
|
||||
@@ -708,8 +708,8 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let label = app.tr_label(c.label);
|
||||
let group = shortcut_group(c);
|
||||
let hit = if filtering {
|
||||
crate::prefs_search::shows_all(ui)
|
||||
|| crate::prefs_search::matches_text(
|
||||
crate::ui_kit::settings_search::shows_all(ui)
|
||||
|| crate::ui_kit::settings_search::matches_text(
|
||||
&q,
|
||||
&[c.label, c.id, label, &group]
|
||||
.into_iter()
|
||||
@@ -761,8 +761,8 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::section_heading(ui, group);
|
||||
let heading = crate::scenario::rect_of(&format!("dialog:heading:{group}"));
|
||||
if let Some(rect) = heading {
|
||||
crate::prefs_search::remember(ui, group, rect);
|
||||
crate::prefs_search::follow(ui, group, rect);
|
||||
crate::ui_kit::settings_search::remember(ui, group, rect);
|
||||
crate::ui_kit::settings_search::follow(ui, group, rect);
|
||||
}
|
||||
for (command, keys, label) in rows {
|
||||
if let Some(chosen) = shortcut_row(app, ui, command, keys, label, filtering) {
|
||||
@@ -777,9 +777,9 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Collapsed matches still participate in Up/Down and Enter. Enter lands on
|
||||
// the real group or command, where follow() reveals its complete row.
|
||||
for (group, rows) in &groups {
|
||||
crate::prefs_search::remember(ui, group, rect);
|
||||
crate::ui_kit::settings_search::remember(ui, group, rect);
|
||||
for (command, keys, label) in rows {
|
||||
crate::prefs_search::remember(ui, &shortcut_search_label(command, label, keys), rect);
|
||||
crate::ui_kit::settings_search::remember(ui, &shortcut_search_label(command, label, keys), rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -915,13 +915,13 @@ fn shortcut_row(
|
||||
let height = if beside { label_height.max(control_height) } else { label_height + control_height + t.space_4 } + t.space_8;
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover());
|
||||
let search_label = shortcut_search_label(command, label, keys);
|
||||
crate::prefs_search::follow(ui, &search_label, rect);
|
||||
crate::ui_kit::settings_search::follow(ui, &search_label, rect);
|
||||
if !filtering && !capturing && !ui.is_rect_visible(rect) {
|
||||
return None;
|
||||
}
|
||||
if filtering {
|
||||
crate::prefs_search::reveal(ui);
|
||||
crate::prefs_search::push_force(ui);
|
||||
crate::ui_kit::settings_search::reveal(ui);
|
||||
crate::ui_kit::settings_search::push_force(ui);
|
||||
}
|
||||
let mut action = None;
|
||||
ui.push_id(command.id, |ui| {
|
||||
@@ -999,10 +999,10 @@ fn shortcut_row(
|
||||
});
|
||||
}
|
||||
});
|
||||
crate::prefs_search::remember(ui, &search_label, rect);
|
||||
crate::ui_kit::settings_search::remember(ui, &search_label, rect);
|
||||
t.secondary_hline(ui.painter(), rect.x_range(), rect.bottom(), t.dialog_rule);
|
||||
if filtering {
|
||||
crate::prefs_search::pop_force(ui);
|
||||
crate::ui_kit::settings_search::pop_force(ui);
|
||||
}
|
||||
action
|
||||
}
|
||||
|
||||
+17
-11
@@ -7,7 +7,7 @@
|
||||
//! right of the viewport, the command palette and a bottom or left-sidebar feature timeline.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::ui_kit::{Container, Surface, recent_colors};
|
||||
|
||||
pub mod access;
|
||||
pub mod agent_chat;
|
||||
@@ -209,7 +209,6 @@ pub(crate) mod plugins;
|
||||
pub mod point_clouds;
|
||||
pub mod prefs;
|
||||
pub mod prefs_notice;
|
||||
mod prefs_search;
|
||||
#[cfg(test)]
|
||||
mod preselect_tests;
|
||||
pub mod preview;
|
||||
@@ -222,7 +221,6 @@ mod print_face_ui;
|
||||
pub(crate) mod quick_measure;
|
||||
#[cfg(test)]
|
||||
mod quick_measure_tests;
|
||||
pub(crate) mod recent_colors;
|
||||
pub mod recovery_ui;
|
||||
pub mod ref_images;
|
||||
pub(crate) mod refused_part;
|
||||
@@ -349,7 +347,7 @@ pub const ORTHO_FACE_TOLERANCE: f64 = 1.0_f64.to_radians();
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct UiState {
|
||||
/// Effective shell mode; automation can override the saved preference for this session.
|
||||
pub layout: shell::LayoutMode,
|
||||
pub layout: ui_kit::LayoutMode,
|
||||
/// Ribbon tab; SHEET METAL and the contextual SKETCH join the seven design tabs.
|
||||
pub tab: String,
|
||||
/// The toolbar tab a sketch was started from; finishing the sketch goes back to it (#3250).
|
||||
@@ -837,6 +835,12 @@ impl Drop for SolveApp {
|
||||
}
|
||||
|
||||
impl SolveApp {
|
||||
/// Publish this frame's tab accent from the open tab, the sketch being edited and the active
|
||||
/// tool or dialog, the three values [`theme::TabAccent::publish`] takes.
|
||||
pub(crate) fn publish_tab_accent(&self, ctx: &egui::Context) {
|
||||
theme::TabAccent::publish(&self.ui.tab, self.session.active_sketch.is_some(), self.tool.is_some() || self.dialog.is_some(), ctx);
|
||||
}
|
||||
|
||||
pub fn new(mut session: Session, services: Services) -> Self {
|
||||
#[cfg(test)]
|
||||
let image_host = Some(solidcraft_engine::render::job::CpuHost::cooperative());
|
||||
@@ -1485,7 +1489,7 @@ impl SolveApp {
|
||||
}
|
||||
if id == "view.layout" {
|
||||
let layout =
|
||||
if v.get("mode").and_then(Value::as_str) == Some("compact") { shell::LayoutMode::Docked } else { shell::LayoutMode::Floating };
|
||||
if v.get("mode").and_then(Value::as_str) == Some("compact") { ui_kit::LayoutMode::Docked } else { ui_kit::LayoutMode::Floating };
|
||||
self.preferences.layout = layout;
|
||||
self.ui.layout = layout;
|
||||
}
|
||||
@@ -1966,7 +1970,7 @@ impl SolveApp {
|
||||
return;
|
||||
}
|
||||
if id == "view.layout" {
|
||||
let mode = if self.ui.layout == shell::LayoutMode::Floating { "compact" } else { "floating" };
|
||||
let mode = if self.ui.layout == ui_kit::LayoutMode::Floating { "compact" } else { "floating" };
|
||||
let _ = self.run(id, json!({"mode":mode}));
|
||||
return;
|
||||
}
|
||||
@@ -3227,6 +3231,7 @@ impl SolveApp {
|
||||
|
||||
/// Per-frame logic before layout.
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
params_dialog::install_expressions(ctx);
|
||||
// Shortcuts run here, before `ui` installs the language scope: a tool started by its
|
||||
// key must build its translated hint in the user's language, not in English.
|
||||
let _language = crate::i18n::scope(&self.preferences.language);
|
||||
@@ -3276,7 +3281,7 @@ impl SolveApp {
|
||||
theme::apply(ctx);
|
||||
self.styled = true;
|
||||
}
|
||||
theme::TabAccent::publish(self, ctx);
|
||||
self.publish_tab_accent(ctx);
|
||||
if !self.fitted {
|
||||
self.fit_view();
|
||||
self.fitted = true;
|
||||
@@ -3403,6 +3408,7 @@ impl SolveApp {
|
||||
|
||||
/// Lay out the whole window.
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
params_dialog::install_expressions(ui.ctx());
|
||||
prefs::prepare_blur(self, ui.ctx());
|
||||
render_workspace::displayed::begin(self);
|
||||
let blur_source = !self.home.open && !render_workspace::result_active(self);
|
||||
@@ -3457,14 +3463,14 @@ impl SolveApp {
|
||||
|
||||
fn cad_ui(&mut self, ui: &mut egui::Ui, report_input: &mut Option<issue_report::CadInput>) {
|
||||
if self.embedded {
|
||||
self.ui.layout = shell::LayoutMode::Docked;
|
||||
self.ui.layout = ui_kit::LayoutMode::Docked;
|
||||
self.home.open = false;
|
||||
self.ui.show_browser = true;
|
||||
self.ui.show_timeline = true;
|
||||
}
|
||||
if self.embed_editor {
|
||||
// The host shows one design and has its own way to start one.
|
||||
self.ui.layout = shell::LayoutMode::Docked;
|
||||
self.ui.layout = ui_kit::LayoutMode::Docked;
|
||||
self.home.open = false;
|
||||
}
|
||||
self.ui.layout.publish(ui.ctx());
|
||||
@@ -3520,7 +3526,7 @@ impl SolveApp {
|
||||
if !floating && !self.embedded {
|
||||
toolbar::toolbar(self, ui);
|
||||
}
|
||||
theme::TabAccent::publish(self, ui.ctx());
|
||||
self.publish_tab_accent(ui.ctx());
|
||||
browser::publish_row_prefs(self, ui.ctx());
|
||||
if !floating {
|
||||
agent_chat::show(self, ui);
|
||||
@@ -3573,7 +3579,7 @@ impl SolveApp {
|
||||
self.show_workspace(&mut viewport_ui);
|
||||
shell::paint_top_corners(ui.painter(), full, theme::Tokens::get().app_bar);
|
||||
toolbar::toolbar(self, ui);
|
||||
theme::TabAccent::publish(self, ui.ctx());
|
||||
self.publish_tab_accent(ui.ctx());
|
||||
}
|
||||
if floating && show_timeline && !left_timeline {
|
||||
timeline::timeline(self, ui);
|
||||
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
(app, component, occurrence, folder, moved)
|
||||
}
|
||||
|
||||
fn menu(app: &SolveApp, target: Target) -> Option<context_menu::Item> {
|
||||
fn menu(app: &SolveApp, target: Target) -> Option<crate::ui_kit::Item> {
|
||||
context_menu::entries(app, &target).into_iter().find(|i| i.id == "ui.relinkSource")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
use crate::{
|
||||
dialogs,
|
||||
scenario::{self, Harness},
|
||||
shell::LayoutMode,
|
||||
theme::Tokens,
|
||||
ui_kit::LayoutMode,
|
||||
};
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::Sel;
|
||||
|
||||
@@ -313,7 +313,7 @@ impl PropertyDraft {
|
||||
}
|
||||
|
||||
fn number(value: &str, field: &str) -> Result<f64, String> {
|
||||
crate::prefs::parse_number(value).ok_or_else(|| format!("{field}: {}", tr("material.editor.number_required")))
|
||||
crate::ui_kit::parse_number(value).ok_or_else(|| format!("{field}: {}", tr("material.editor.number_required")))
|
||||
}
|
||||
|
||||
fn field(ui: &mut egui::Ui, label: &str, id: &str, value: &mut String, unit: &str) {
|
||||
|
||||
@@ -7,7 +7,8 @@ use solidcore_record::Vec3;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::geom::prelude::*;
|
||||
|
||||
use crate::context_menu::{self, Item, Target};
|
||||
use crate::context_menu::{self, Target};
|
||||
use crate::ui_kit::Item;
|
||||
use crate::viewport::Hit;
|
||||
use crate::{Services, SolveApp};
|
||||
|
||||
@@ -2053,7 +2054,7 @@ fn a_flyout_inside_a_flyout_that_opened_to_the_left_opens_to_the_left() {
|
||||
use crate::scenario::rect_of;
|
||||
let (mut h, c) = component_menu();
|
||||
let right = h.ctx.content_rect().right();
|
||||
context_menu::open_for(&mut h.app, pos2(right - context_menu::LIST_W - 50.0, 60.0), Target::Component { id: c });
|
||||
context_menu::open_for(&mut h.app, pos2(right - crate::ui_kit::LIST_W - 50.0, 60.0), Target::Component { id: c });
|
||||
h.frames(2);
|
||||
h.step(&json!({"move":{"handle":"menu:ui.group.component"}})).unwrap();
|
||||
h.frames(2);
|
||||
|
||||
@@ -27,8 +27,9 @@ use egui::{Align2, Color32, FontId, Mesh, Painter, Pos2, Rect, Sense, Shape, Str
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::context_menu::{Item, act, cmd};
|
||||
use crate::context_menu::{act, cmd};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::Item;
|
||||
|
||||
/// Most tools one ring may hold.
|
||||
pub(crate) const MAX_TOOLS: usize = 12;
|
||||
|
||||
@@ -21,22 +21,6 @@ use crate::prefs::{OrbitAround, pivot_under};
|
||||
use crate::space_mouse::{BridgeMotion, Motion};
|
||||
use crate::viewport::{Hit, NavMode, axis_rect, nearest_quarter_turn, pick, pick_cached, projection};
|
||||
|
||||
/// Take a predominantly horizontal wheel gesture for a hovered scroll viewport.
|
||||
/// Leave vertical input for its enclosing page, and leave horizontal input
|
||||
/// unconsumed when this viewport is already at the requested end.
|
||||
pub(crate) fn take_horizontal_wheel(ui: &egui::Ui, offset: f32, max_offset: f32) -> f32 {
|
||||
ui.input_mut(|input| {
|
||||
let delta = input.smooth_scroll_delta;
|
||||
if delta.x == 0.0 || delta.x.abs() <= delta.y.abs() {
|
||||
return 0.0;
|
||||
}
|
||||
if (offset - delta.x).clamp(0.0, max_offset) != offset {
|
||||
input.smooth_scroll_delta.x = 0.0;
|
||||
}
|
||||
delta.x
|
||||
})
|
||||
}
|
||||
|
||||
/// What a page's camera may do.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct NavCaps {
|
||||
@@ -2062,7 +2046,8 @@ mod tests {
|
||||
];
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let dirs = [root.join("src"), root.join("../../apps/solidcraft/src"), root.join("../../apps/solidcraft-web/src")];
|
||||
let allowed = ["navigation.rs"];
|
||||
// The kit's wheel.rs scrolls strips (a horizontal wheel over a hovered scroll area), not cameras.
|
||||
let allowed = ["navigation.rs", "wheel.rs"];
|
||||
let mut offenders = Vec::new();
|
||||
let mut scanned = 0;
|
||||
let mut stack: Vec<std::path::PathBuf> = dirs.iter().filter(|d| d.is_dir()).cloned().collect();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! Stored navigation slots and their catalogue. Painting lives in `navigation_bar_view`.
|
||||
use crate::{SolveApp, navigation::NavCaps};
|
||||
use crate::{
|
||||
SolveApp,
|
||||
navigation::NavCaps,
|
||||
ui_kit::{NavigationEdge, NavigationIdle, NavigationKind, NavigationSize},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -34,18 +38,9 @@ pub(crate) fn visual_style_icon(style: u8) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Kind {
|
||||
Action,
|
||||
Tool,
|
||||
Toggle,
|
||||
Menu,
|
||||
Separator,
|
||||
}
|
||||
|
||||
pub(crate) struct Slot {
|
||||
pub id: &'static str,
|
||||
pub kind: Kind,
|
||||
pub kind: NavigationKind,
|
||||
pub icon: &'static str,
|
||||
pub label: &'static str,
|
||||
pub description: &'static str,
|
||||
@@ -56,7 +51,7 @@ macro_rules! slot {
|
||||
($id:literal, $kind:ident, $icon:literal, $command:expr) => {
|
||||
Slot {
|
||||
id: $id,
|
||||
kind: Kind::$kind,
|
||||
kind: NavigationKind::$kind,
|
||||
icon: $icon,
|
||||
label: concat!("navbar.", $id),
|
||||
description: concat!("navbar.description.", $id),
|
||||
@@ -124,30 +119,6 @@ pub enum Position {
|
||||
Right,
|
||||
}
|
||||
|
||||
/// The edge of the 3D view the bar sits on (#8183): the bottom, or the top, under the ribbon.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Edge {
|
||||
#[default]
|
||||
Bottom,
|
||||
Top,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Size {
|
||||
Compact,
|
||||
#[default]
|
||||
Regular,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
// Logical points: egui's UI zoom applies the preference's scale to these dimensions.
|
||||
pub(crate) fn points(self) -> f32 {
|
||||
crate::ui_kit::NavigationMetrics::for_size(self).button
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Labels {
|
||||
@@ -156,15 +127,6 @@ pub enum Labels {
|
||||
Labels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Idle {
|
||||
Always,
|
||||
#[default]
|
||||
Fade,
|
||||
Hide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MenuDisplay {
|
||||
@@ -183,10 +145,10 @@ pub struct NavigationBar {
|
||||
pub slots: Vec<String>,
|
||||
pub slot_options: std::collections::BTreeMap<String, SlotOptions>,
|
||||
pub position: Position,
|
||||
pub edge: Edge,
|
||||
pub size: Size,
|
||||
pub edge: NavigationEdge,
|
||||
pub size: NavigationSize,
|
||||
pub labels: Labels,
|
||||
pub idle: Idle,
|
||||
pub idle: NavigationIdle,
|
||||
}
|
||||
|
||||
impl Default for NavigationBar {
|
||||
@@ -198,10 +160,10 @@ impl Default for NavigationBar {
|
||||
.collect(),
|
||||
slot_options: Default::default(),
|
||||
position: Position::default(),
|
||||
edge: Edge::default(),
|
||||
size: Size::default(),
|
||||
edge: NavigationEdge::default(),
|
||||
size: NavigationSize::default(),
|
||||
labels: Labels::default(),
|
||||
idle: Idle::default(),
|
||||
idle: NavigationIdle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +195,7 @@ impl<'de> Deserialize<'de> for NavigationBar {
|
||||
}
|
||||
if let Some(options) = value.get("slot_options").and_then(Value::as_object) {
|
||||
for (id, option) in options.iter().take(MAX_SLOTS) {
|
||||
if id.len() <= 128 && find(id).is_some_and(|slot| slot.kind == Kind::Menu) {
|
||||
if id.len() <= 128 && find(id).is_some_and(|slot| slot.kind == NavigationKind::Menu) {
|
||||
let shows = option.get("shows").cloned().and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default();
|
||||
bar.slot_options.insert(id.clone(), SlotOptions { shows });
|
||||
}
|
||||
@@ -274,10 +236,10 @@ pub(crate) fn validate(value: &Value) -> Result<(), String> {
|
||||
})
|
||||
}),
|
||||
"position" => serde_json::from_value::<Position>(value.clone()).is_ok(),
|
||||
"edge" => serde_json::from_value::<Edge>(value.clone()).is_ok(),
|
||||
"size" => serde_json::from_value::<Size>(value.clone()).is_ok(),
|
||||
"edge" => serde_json::from_value::<NavigationEdge>(value.clone()).is_ok(),
|
||||
"size" => serde_json::from_value::<NavigationSize>(value.clone()).is_ok(),
|
||||
"labels" => serde_json::from_value::<Labels>(value.clone()).is_ok(),
|
||||
"idle" => serde_json::from_value::<Idle>(value.clone()).is_ok(),
|
||||
"idle" => serde_json::from_value::<NavigationIdle>(value.clone()).is_ok(),
|
||||
_ => return Err(format!("unknown navigation_bar field `{key}`")),
|
||||
};
|
||||
if !valid {
|
||||
|
||||
@@ -38,7 +38,7 @@ fn slot_catalogue_uses_registered_commands_and_existing_icons() {
|
||||
Some(command) => assert!(solidcraft_engine::find_command(command).is_some(), "{}", slot.id),
|
||||
None => assert!(["projection", "display", "separator"].contains(&slot.id)),
|
||||
}
|
||||
if slot.kind != Kind::Separator {
|
||||
if slot.kind != NavigationKind::Separator {
|
||||
assert!(!crate::icon_svg::resolve(slot.icon).placeholder, "{}: {}", slot.id, slot.icon);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ fn measured_bar_drops_labels_then_overflows_in_order_without_compass_overlap() {
|
||||
#[test]
|
||||
fn measured_bar_honours_size_position_and_ignores_unknown_slots() {
|
||||
let canvas = Rect::from_min_size(pos2(20.0, 30.0), vec2(700.0, 500.0));
|
||||
for size in [Size::Compact, Size::Regular] {
|
||||
for size in [NavigationSize::Compact, NavigationSize::Regular] {
|
||||
for position in [Position::Centre, Position::Left, Position::Right] {
|
||||
let prefs: NavigationBar = serde_json::from_value(json!({"slots":["future", "pan", "fit"], "size":size, "position":position})).unwrap();
|
||||
let bar = measure(canvas, &prefs, NavCaps::MODEL, |_| 0.0);
|
||||
@@ -156,20 +156,20 @@ fn a_bar_on_the_top_edge_sits_under_the_top_and_keeps_the_corner_controls_below_
|
||||
use crate::ui_kit::NAV_BOTTOM;
|
||||
// Older settings have no edge and keep the bar at the bottom; a bad value recovers the same way.
|
||||
let old: NavigationBar = serde_json::from_value(json!({"position": "right"})).unwrap();
|
||||
assert_eq!(old.edge, Edge::Bottom);
|
||||
assert_eq!(old.edge, NavigationEdge::Bottom);
|
||||
let bad: NavigationBar = serde_json::from_value(json!({"edge": "sideways"})).unwrap();
|
||||
assert_eq!(bad.edge, Edge::Bottom);
|
||||
assert_eq!(bad.edge, NavigationEdge::Bottom);
|
||||
assert!(validate(&json!({"edge": "sideways"})).is_err());
|
||||
assert!(validate(&json!({"edge": "top"})).is_ok());
|
||||
let canvas = Rect::from_min_size(pos2(20.0, 30.0), vec2(900.0, 600.0));
|
||||
for position in [Position::Centre, Position::Left, Position::Right] {
|
||||
for edge in [Edge::Bottom, Edge::Top] {
|
||||
for edge in [NavigationEdge::Bottom, NavigationEdge::Top] {
|
||||
let prefs: NavigationBar = serde_json::from_value(json!({"position": position, "edge": edge})).unwrap();
|
||||
assert_eq!(serde_json::to_value(&prefs).unwrap()["edge"], if edge == Edge::Top { "top" } else { "bottom" });
|
||||
assert_eq!(serde_json::to_value(&prefs).unwrap()["edge"], if edge == NavigationEdge::Top { "top" } else { "bottom" });
|
||||
let bar = measure(canvas, &prefs, NavCaps::MODEL, |_| 50.0);
|
||||
match edge {
|
||||
Edge::Bottom => assert_eq!(bar.rect.bottom(), canvas.bottom() - NAV_BOTTOM),
|
||||
Edge::Top => assert_eq!(bar.rect.top(), canvas.top() + NAV_BOTTOM),
|
||||
NavigationEdge::Bottom => assert_eq!(bar.rect.bottom(), canvas.bottom() - NAV_BOTTOM),
|
||||
NavigationEdge::Top => assert_eq!(bar.rect.top(), canvas.top() + NAV_BOTTOM),
|
||||
}
|
||||
for corner in [egui::Vec2::ZERO, vec2(120.0, 32.0)] {
|
||||
let layout =
|
||||
@@ -187,7 +187,7 @@ fn a_bar_on_the_top_edge_sits_under_the_top_and_keeps_the_corner_controls_below_
|
||||
#[test]
|
||||
fn navigation_slot_visuals_never_use_focus_as_a_fill() {
|
||||
use crate::ui_kit::{NavigationFill, navigation_visual};
|
||||
for kind in [Kind::Action, Kind::Tool, Kind::Toggle, Kind::Menu] {
|
||||
for kind in [NavigationKind::Action, NavigationKind::Tool, NavigationKind::Toggle, NavigationKind::Menu] {
|
||||
for keyboard_focus in [false, true] {
|
||||
assert_eq!(
|
||||
navigation_visual(kind, false, false, false, keyboard_focus),
|
||||
@@ -202,8 +202,8 @@ fn navigation_slot_visuals_never_use_focus_as_a_fill() {
|
||||
);
|
||||
}
|
||||
let active_fill = match kind {
|
||||
Kind::Action => NavigationFill::None,
|
||||
Kind::Menu => NavigationFill::Press,
|
||||
NavigationKind::Action => NavigationFill::None,
|
||||
NavigationKind::Menu => NavigationFill::Press,
|
||||
_ => NavigationFill::Active,
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -236,7 +236,7 @@ fn navigation_slot_pointer_clicks_release_focus_and_leave_no_fill() {
|
||||
fill
|
||||
}
|
||||
|
||||
for kind in [Kind::Action, Kind::Tool, Kind::Toggle] {
|
||||
for kind in [NavigationKind::Action, NavigationKind::Tool, NavigationKind::Toggle] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let bounds = Rect::from_min_size(pos2(20.0, 30.0), vec2(26.0, 26.0));
|
||||
let draw = |events, selected| {
|
||||
@@ -271,7 +271,7 @@ fn navigation_slot_pointer_clicks_release_focus_and_leave_no_fill() {
|
||||
assert!(!released.1, "{kind:?}: pointer activation must surrender focus");
|
||||
let away = draw(vec![Event::PointerMoved(pos2(200.0, 200.0))], false);
|
||||
assert_eq!(fill(&away.0, bounds), None, "{kind:?}: no lingering fill");
|
||||
if kind != Kind::Action {
|
||||
if kind != NavigationKind::Action {
|
||||
let active = draw(vec![], true);
|
||||
assert_eq!(fill(&active.0, bounds), Some(Tokens::get().selected_bg));
|
||||
draw(crate::test_support::pointer(bounds.center(), true), true);
|
||||
@@ -399,7 +399,7 @@ fn navigation_preview_and_viewport_share_the_row_painter() {
|
||||
&& live != preview
|
||||
{
|
||||
let first = live.vertices.first().unwrap().pos;
|
||||
assert!(bar.slots.iter().any(|(slot, rect)| slot.kind == Kind::Menu && rect.contains(first)));
|
||||
assert!(bar.slots.iter().any(|(slot, rect)| slot.kind == NavigationKind::Menu && rect.contains(first)));
|
||||
let delta = preview.vertices.first().unwrap().pos - first;
|
||||
assert_eq!(delta.y, 0.0);
|
||||
assert_eq!(delta.x.abs(), crate::ui_kit::NAV_BORDER, "editing menus include their CSS transparent border");
|
||||
@@ -484,8 +484,10 @@ fn navigation_context_menus_customise_hide_reset_and_remove() {
|
||||
#[test]
|
||||
fn navigation_board_metrics_and_state_colors_are_shared_in_both_themes() {
|
||||
use crate::ui_kit::*;
|
||||
assert_eq!(Size::default(), Size::Regular);
|
||||
for (size, button, icon, height, radius, button_radius) in [(Size::Regular, 32.0, 18.0, 40.0, 10, 7), (Size::Compact, 26.0, 16.0, 34.0, 9, 6)] {
|
||||
assert_eq!(NavigationSize::default(), NavigationSize::Regular);
|
||||
for (size, button, icon, height, radius, button_radius) in
|
||||
[(NavigationSize::Regular, 32.0, 18.0, 40.0, 10, 7), (NavigationSize::Compact, 26.0, 16.0, 34.0, 9, 6)]
|
||||
{
|
||||
let metrics = NavigationMetrics::for_size(size);
|
||||
assert_eq!(
|
||||
(metrics.button, metrics.icon, metrics.height(), metrics.radius, metrics.button_radius),
|
||||
@@ -513,7 +515,7 @@ fn navigation_board_metrics_and_state_colors_are_shared_in_both_themes() {
|
||||
assert_eq!(frame.shadow, Container::menu_frame().shadow, "navigation inherits the shared popup elevation");
|
||||
}
|
||||
assert_eq!((NAV_POPUP_WIDTH, NAV_CONTEXT_WIDTH, NAV_MORE_WIDTH, NAV_POPUP_GAP), (244.0, 232.0, 200.0, 6.0));
|
||||
for (idle, opacity) in [(Idle::Always, 1.0), (Idle::Fade, 0.45), (Idle::Hide, 0.0)] {
|
||||
for (idle, opacity) in [(NavigationIdle::Always, 1.0), (NavigationIdle::Fade, 0.45), (NavigationIdle::Hide, 0.0)] {
|
||||
assert_eq!(navigation_opacity(idle, false), opacity);
|
||||
assert_eq!(navigation_opacity(idle, true), 1.0);
|
||||
}
|
||||
@@ -585,12 +587,12 @@ fn navigation_board_position_aliases_migrate_and_editor_alignment_follows_positi
|
||||
let bar: NavigationBar = serde_json::from_value(json!({"position":legacy,"slots":["orbit","pan","fit","look_at","visual_style"]})).unwrap();
|
||||
assert_eq!(bar.position, position);
|
||||
assert_eq!(serde_json::to_value(&bar).unwrap()["position"], current);
|
||||
for size in [Size::Compact, Size::Regular] {
|
||||
for size in [NavigationSize::Compact, NavigationSize::Regular] {
|
||||
let bar = NavigationBar { size, ..bar.clone() };
|
||||
let preview = editor_layout(stage, &bar, |_| 0.0);
|
||||
let live = measure(stage, &bar, NavCaps::MODEL, |_| 0.0);
|
||||
assert_eq!(preview.rect.bottom(), stage.bottom() - NAV_BORDER - NAV_STAGE_INSET);
|
||||
let top = editor_layout(stage, &NavigationBar { edge: crate::navigation_bar::Edge::Top, ..bar.clone() }, |_| 0.0);
|
||||
let top = editor_layout(stage, &NavigationBar { edge: NavigationEdge::Top, ..bar.clone() }, |_| 0.0);
|
||||
assert_eq!(top.rect.top(), stage.top() + NAV_BORDER + NAV_STAGE_INSET);
|
||||
assert_eq!(preview.rect.height(), NavigationMetrics::for_size(size).height());
|
||||
match position {
|
||||
@@ -601,7 +603,7 @@ fn navigation_board_position_aliases_migrate_and_editor_alignment_follows_positi
|
||||
assert_eq!(preview.slots.iter().map(|(slot, _)| slot.id).collect::<Vec<_>>(), bar.slots);
|
||||
for ((slot, rect), (live_slot, live_rect)) in preview.slots.iter().zip(&live.slots) {
|
||||
assert_eq!(slot.id, live_slot.id);
|
||||
assert_eq!(rect.width(), live_rect.width() + if slot.kind == Kind::Menu { 2.0 * NAV_BORDER } else { 0.0 });
|
||||
assert_eq!(rect.width(), live_rect.width() + if slot.kind == NavigationKind::Menu { 2.0 * NAV_BORDER } else { 0.0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Measured single-row navigation bar. Commands own all view changes.
|
||||
use crate::navigation::NavCaps;
|
||||
use crate::navigation_bar::{self, Edge, Idle, Kind, Labels, MenuDisplay, NavigationBar, Position, Slot};
|
||||
use crate::navigation_bar::{self, Labels, MenuDisplay, NavigationBar, Position, Slot};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::{self, Container, Surface};
|
||||
use crate::ui_kit::{self, Container, NavigationEdge, NavigationIdle, NavigationKind, Surface};
|
||||
use crate::{SolveApp, viewport::NavMode};
|
||||
use egui::{Align2, Rect, pos2, vec2};
|
||||
use serde_json::json;
|
||||
@@ -39,9 +39,9 @@ fn measure_row(content: Rect, prefs: &NavigationBar, caps: NavCaps, label_width:
|
||||
return result;
|
||||
}
|
||||
let width = |slot: &Slot, labels, values| {
|
||||
if slot.kind == Kind::Separator {
|
||||
if slot.kind == NavigationKind::Separator {
|
||||
NAV_SEPARATOR
|
||||
} else if slot.kind == Kind::Menu {
|
||||
} else if slot.kind == NavigationKind::Menu {
|
||||
metrics.icon
|
||||
+ if editing { 2.0 * NAV_BORDER } else { 0.0 }
|
||||
+ NAV_MENU_LEFT
|
||||
@@ -91,8 +91,8 @@ fn measure_row(content: Rect, prefs: &NavigationBar, caps: NavCaps, label_width:
|
||||
};
|
||||
// The same gap from either edge of the view.
|
||||
let top = match prefs.edge {
|
||||
navigation_bar::Edge::Bottom => content.bottom() - height - NAV_BOTTOM,
|
||||
navigation_bar::Edge::Top => content.top() + NAV_BOTTOM,
|
||||
NavigationEdge::Bottom => content.bottom() - height - NAV_BOTTOM,
|
||||
NavigationEdge::Top => content.top() + NAV_BOTTOM,
|
||||
};
|
||||
result.rect = Rect::from_min_size(pos2(left, top), vec2(row_width, height));
|
||||
let mut x = left + inset;
|
||||
@@ -122,8 +122,8 @@ pub(crate) fn editor_layout(stage: Rect, prefs: &NavigationBar, label_width: imp
|
||||
};
|
||||
// The same inset from the top or bottom edge as from the side, as in the live view.
|
||||
let y = match prefs.edge {
|
||||
navigation_bar::Edge::Top => stage.top() + ui_kit::NAV_BORDER + ui_kit::NAV_STAGE_INSET,
|
||||
navigation_bar::Edge::Bottom => stage.bottom() - ui_kit::NAV_BORDER - ui_kit::NAV_STAGE_INSET - bar.rect.height(),
|
||||
NavigationEdge::Top => stage.top() + ui_kit::NAV_BORDER + ui_kit::NAV_STAGE_INSET,
|
||||
NavigationEdge::Bottom => stage.bottom() - ui_kit::NAV_BORDER - ui_kit::NAV_STAGE_INSET - bar.rect.height(),
|
||||
};
|
||||
let delta = pos2(x, y) - bar.rect.min;
|
||||
bar.rect = bar.rect.translate(delta);
|
||||
@@ -249,7 +249,7 @@ fn activate(app: &mut SolveApp, slot: &Slot) {
|
||||
_ => {
|
||||
if let Some(command) = slot.command {
|
||||
let params = match slot.kind {
|
||||
Kind::Tool if command == "view.tool" => json!({"tool": if selected(app, slot.id) { "none" } else { slot.id }}),
|
||||
NavigationKind::Tool if command == "view.tool" => json!({"tool": if selected(app, slot.id) { "none" } else { slot.id }}),
|
||||
_ if slot.id == "ambient_occlusion" => json!({"enabled": !app.preferences.ambient_occlusion}),
|
||||
_ => json!({}),
|
||||
};
|
||||
@@ -259,7 +259,7 @@ fn activate(app: &mut SolveApp, slot: &Slot) {
|
||||
}
|
||||
}
|
||||
|
||||
fn tooltip(app: &SolveApp, response: egui::Response, slot: &Slot, hidden: Option<&str>, along: Rect, edge: Edge) -> egui::Response {
|
||||
fn tooltip(app: &SolveApp, response: egui::Response, slot: &Slot, hidden: Option<&str>, along: Rect, edge: NavigationEdge) -> egui::Response {
|
||||
let key = slot.command.and_then(|id| crate::keymap::display_for(app, id));
|
||||
let label = if let Some(hidden) = hidden {
|
||||
app.tr(hidden).to_owned()
|
||||
@@ -299,7 +299,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
|| !open.is_empty();
|
||||
let opacity = ui_kit::navigation_opacity(app.preferences.navigation_bar.idle, active);
|
||||
let background = ui.interact(bar.rect, ui.scope_id().with("navigation_bar_context"), egui::Sense::click());
|
||||
let opacity = if app.preferences.navigation_bar.idle == Idle::Hide {
|
||||
let opacity = if app.preferences.navigation_bar.idle == NavigationIdle::Hide {
|
||||
ui.ctx().animate_value_with_time(menu_id.with("opacity"), opacity, if active { ui_kit::NAV_FADE_SECONDS } else { 0.0 })
|
||||
} else {
|
||||
opacity
|
||||
@@ -311,8 +311,8 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let bounds = response.rect;
|
||||
crate::scenario::publish_target(&format!("nav:slot:{}", slot.id), bounds);
|
||||
context_menu(app, &response, Some(index));
|
||||
if response.clicked() && slot.kind != Kind::Separator {
|
||||
if slot.kind == Kind::Menu {
|
||||
if response.clicked() && slot.kind != NavigationKind::Separator {
|
||||
if slot.kind == NavigationKind::Menu {
|
||||
open = if open == slot.id { String::new() } else { slot.id.to_owned() };
|
||||
} else {
|
||||
activate(app, slot);
|
||||
@@ -331,7 +331,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
bar.more,
|
||||
ui_kit::NavigationButton {
|
||||
id: "more",
|
||||
kind: Kind::Menu,
|
||||
kind: NavigationKind::Menu,
|
||||
icon: "dots",
|
||||
label: app.tr("navbar.more"),
|
||||
selected: open == "more" || bar.overflow.iter().any(|slot| slot.id == open),
|
||||
@@ -353,8 +353,8 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let ctx = ui.ctx().clone();
|
||||
// Menus open into the view: up from a bar at the bottom, down from one at the top.
|
||||
let (pivot, at) = match app.preferences.navigation_bar.edge {
|
||||
Edge::Bottom => (Align2::RIGHT_BOTTOM, anchor.right_top() - vec2(0.0, ui_kit::NAV_POPUP_GAP)),
|
||||
Edge::Top => (Align2::RIGHT_TOP, anchor.right_bottom() + vec2(0.0, ui_kit::NAV_POPUP_GAP)),
|
||||
NavigationEdge::Bottom => (Align2::RIGHT_BOTTOM, anchor.right_top() - vec2(0.0, ui_kit::NAV_POPUP_GAP)),
|
||||
NavigationEdge::Top => (Align2::RIGHT_TOP, anchor.right_bottom() + vec2(0.0, ui_kit::NAV_POPUP_GAP)),
|
||||
};
|
||||
let menu = Container::new(&ctx, Surface::Popup).area(menu_id.with("popup")).order(egui::Order::Foreground).pivot(pivot).fixed_pos(at).show(
|
||||
&ctx,
|
||||
@@ -426,7 +426,7 @@ pub(crate) fn paint_row(
|
||||
} else if editing.is_none() && slot.id == "projection" && projection_locked(app) {
|
||||
ui.set_opacity(ui.visuals().disabled_alpha);
|
||||
}
|
||||
if slot.kind == Kind::Separator {
|
||||
if slot.kind == NavigationKind::Separator {
|
||||
ui_kit::navigation_separator(ui, *bounds);
|
||||
ui.interact(
|
||||
*bounds,
|
||||
@@ -440,14 +440,14 @@ pub(crate) fn paint_row(
|
||||
icon: slot.current_icon(app.ui.visual_style),
|
||||
label: slot_text(app, slot),
|
||||
selected: if editing.is_some() { false } else { selected(app, slot.id) || open == slot.id },
|
||||
labels: if slot.kind == Kind::Menu {
|
||||
labels: if slot.kind == NavigationKind::Menu {
|
||||
bar.values
|
||||
&& slot.id == "visual_style"
|
||||
&& app.preferences.navigation_bar.menu_display(slot.id) == MenuDisplay::IconAndValue
|
||||
} else {
|
||||
bar.labels
|
||||
},
|
||||
menu: slot.kind == Kind::Menu,
|
||||
menu: slot.kind == NavigationKind::Menu,
|
||||
};
|
||||
if editing.is_some() {
|
||||
ui_kit::navigation_button_editable(ui, *bounds, button, true)
|
||||
@@ -472,7 +472,7 @@ pub(crate) fn paint_row(
|
||||
});
|
||||
}
|
||||
// The settings' preview keeps its tips above, wherever the bar sits in the view.
|
||||
let edge = if editing.is_some() { Edge::Bottom } else { app.preferences.navigation_bar.edge };
|
||||
let edge = if editing.is_some() { NavigationEdge::Bottom } else { app.preferences.navigation_bar.edge };
|
||||
responses.push((index, *slot, tooltip(app, response, slot, hidden, bar.rect, edge)));
|
||||
}
|
||||
responses
|
||||
@@ -534,7 +534,7 @@ fn style_menu(app: &mut SolveApp, ui: &mut egui::Ui) -> bool {
|
||||
|
||||
fn overflow_menu(app: &mut SolveApp, ui: &mut egui::Ui, slots: &[&Slot], open: &mut String) {
|
||||
for slot in slots {
|
||||
if slot.kind == Kind::Separator {
|
||||
if slot.kind == NavigationKind::Separator {
|
||||
let start = ui.next_widget_position();
|
||||
ui_kit::navigation_menu_separator(ui);
|
||||
let bounds = Rect::from_min_max(start, pos2(start.x + ui.available_width(), ui.next_widget_position().y));
|
||||
@@ -542,7 +542,7 @@ fn overflow_menu(app: &mut SolveApp, ui: &mut egui::Ui, slots: &[&Slot], open: &
|
||||
continue;
|
||||
}
|
||||
let mut item = ui_kit::NavigationMenuRow::new(slot.id, app.tr(slot.label), slot.current_icon(app.ui.visual_style));
|
||||
if matches!(slot.kind, Kind::Tool | Kind::Toggle) {
|
||||
if matches!(slot.kind, NavigationKind::Tool | NavigationKind::Toggle) {
|
||||
item.selected = selected(app, slot.id);
|
||||
}
|
||||
let response = item.show(ui);
|
||||
@@ -552,8 +552,8 @@ fn overflow_menu(app: &mut SolveApp, ui: &mut egui::Ui, slots: &[&Slot], open: &
|
||||
if slot.id == "display" {
|
||||
crate::scenario::publish_handle("nav:view", response.rect.center());
|
||||
}
|
||||
if tooltip(app, response.clone(), slot, None, response.rect, Edge::Bottom).clicked() {
|
||||
if slot.kind == Kind::Menu {
|
||||
if tooltip(app, response.clone(), slot, None, response.rect, NavigationEdge::Bottom).clicked() {
|
||||
if slot.kind == NavigationKind::Menu {
|
||||
*open = slot.id.to_owned();
|
||||
} else {
|
||||
activate(app, slot);
|
||||
|
||||
@@ -975,7 +975,7 @@ impl crate::SolveApp {
|
||||
self.shell.history_dock_space = None;
|
||||
self.shell.history_folding = None;
|
||||
self.shell.history_fold_motion = None;
|
||||
crate::theme::TabAccent::publish(self, ui.ctx());
|
||||
self.publish_tab_accent(ui.ctx());
|
||||
crate::browser::publish_row_prefs(self, ui.ctx());
|
||||
let mut placement = render_domain;
|
||||
self.shell.status_bar = if crate::status_bar::shown(self) {
|
||||
|
||||
@@ -1273,7 +1273,7 @@ fn allocation_invisible_physical_edges_and_measured_tab_halves_preserve_rtl_and_
|
||||
|
||||
#[test]
|
||||
fn allocation_shared_shell_gap_controls_allocation_and_keeps_compact_sashes_and_free_origins() {
|
||||
for mode in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
for direction in [Direction::Ltr, Direction::Rtl] {
|
||||
for zone in [Zone::Left, Zone::Right] {
|
||||
let gap = crate::ui_kit::Container::for_layout(mode, crate::ui_kit::Surface::Panel).gap();
|
||||
@@ -3190,13 +3190,13 @@ fn raw_host_wider_perimeters_win_over_cards_tabs_and_clear_below_without_fallbac
|
||||
// Exercise both actual shell gaps without multiplying all
|
||||
// selection cases by an unrelated rendering-mode matrix.
|
||||
let mode = if matches!(case, "side-edge" | "bottom-clear" | "empty") {
|
||||
crate::shell::LayoutMode::Docked
|
||||
crate::ui_kit::LayoutMode::Docked
|
||||
} else {
|
||||
crate::shell::LayoutMode::Floating
|
||||
crate::ui_kit::LayoutMode::Floating
|
||||
};
|
||||
mode.publish(&raw.ctx);
|
||||
let gap = crate::ui_kit::Container::for_layout(mode, crate::ui_kit::Surface::Panel).gap();
|
||||
assert_eq!(gap, if mode == crate::shell::LayoutMode::Docked { 0.0 } else { 7.0 });
|
||||
assert_eq!(gap, if mode == crate::ui_kit::LayoutMode::Docked { 0.0 } else { 7.0 });
|
||||
let mut options = priority_host_options();
|
||||
options.metrics.gap = gap;
|
||||
options.metrics.inline_max_height = 0.0;
|
||||
@@ -3434,7 +3434,7 @@ fn raw_host_perimeter_anchors_survive_source_detach_and_commit_the_exact_preview
|
||||
#[test]
|
||||
fn raw_host_drag_and_pair_resize_dispatch_once_even_when_egui_repeats_the_release_pass() {
|
||||
for (mode, resize) in
|
||||
[(crate::shell::LayoutMode::Floating, false), (crate::shell::LayoutMode::Floating, true), (crate::shell::LayoutMode::Docked, true)]
|
||||
[(crate::ui_kit::LayoutMode::Floating, false), (crate::ui_kit::LayoutMode::Floating, true), (crate::ui_kit::LayoutMode::Docked, true)]
|
||||
{
|
||||
let h = host_logs(2, Zone::Left);
|
||||
let mut raw = RawPanelHost::new(h, 2.0);
|
||||
@@ -12384,7 +12384,7 @@ fn app_atomic_workspace_replacements_accept_distinct_incoming_contexts_and_match
|
||||
|
||||
#[test]
|
||||
fn captured_renderer_alpha_reaches_new_sections_and_alpha_only_drift_invalidates_owned_geometry() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let (mut h, _, _) = boundary_app();
|
||||
h.app.ui.layout = mode;
|
||||
h.frames(2);
|
||||
@@ -12402,7 +12402,7 @@ fn captured_renderer_alpha_reaches_new_sections_and_alpha_only_drift_invalidates
|
||||
assert_eq!(card.paint_alpha, Some(255), "Classic's default opaque policy is preserved");
|
||||
}
|
||||
let gap = crate::ui_kit::Container::for_layout(mode, crate::ui_kit::Surface::Panel).gap();
|
||||
assert_eq!(gap, if mode == crate::shell::LayoutMode::Floating { 7.0 } else { 0.0 });
|
||||
assert_eq!(gap, if mode == crate::ui_kit::LayoutMode::Floating { 7.0 } else { 0.0 });
|
||||
assert_eq!(painted.metrics.gap, gap);
|
||||
assert_eq!(painted.allocation_environment.gap, gap);
|
||||
let domain = egui_rect(painted.render_domain);
|
||||
|
||||
@@ -1024,6 +1024,32 @@ pub fn complete(ui: &mut egui::Ui, resp: &egui::Response, text: &mut String) ->
|
||||
crate::expression_complete::complete(ui, resp, text) || crate::value_step::step(ui, resp, text)
|
||||
}
|
||||
|
||||
/// SolidCraft's expression language for the kit's value fields: literals and units as
|
||||
/// `doc::expr` reads them, completion and stepping by [`complete`].
|
||||
pub(crate) struct DocExpressions;
|
||||
|
||||
impl ui_kit::Expressions for DocExpressions {
|
||||
fn is_literal(&self, text: &str) -> bool {
|
||||
solidcraft_engine::doc::expr::is_literal(text)
|
||||
}
|
||||
|
||||
fn is_unit(&self, unit: &str) -> bool {
|
||||
solidcraft_engine::doc::expr::unit_info(unit).is_some()
|
||||
}
|
||||
|
||||
fn complete(&self, ui: &mut egui::Ui, response: &egui::Response, text: &mut String) -> bool {
|
||||
complete(ui, response, text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Give the kit's value fields in `ctx` SolidCraft's expressions, unless they have them already.
|
||||
/// The app calls this at the start of each frame entry point, so every context it draws into has them.
|
||||
pub(crate) fn install_expressions(ctx: &egui::Context) {
|
||||
if ui_kit::expressions(ctx).is_none() {
|
||||
ui_kit::install_expressions(ctx, Arc::new(DocExpressions));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -157,7 +157,7 @@ fn page_in_dir(app: &mut SolveApp, ui: &mut egui::Ui, dir: Option<&Path>) {
|
||||
};
|
||||
let t = crate::theme::Tokens::get();
|
||||
let path = dir.display().to_string();
|
||||
let searching = crate::prefs_search::querying(ui);
|
||||
let searching = crate::ui_kit::settings_search::querying(ui);
|
||||
// The page's own box is for browsing Plugins. The dialog search is the filter while it is active.
|
||||
if !searching {
|
||||
crate::ui_kit::search_field(
|
||||
@@ -176,26 +176,26 @@ fn page_in_dir(app: &mut SolveApp, ui: &mut egui::Ui, dir: Option<&Path>) {
|
||||
crate::i18n::tr("prefs.plugins_installed"),
|
||||
Some(&plugins.len().to_string()),
|
||||
|ui| {
|
||||
if plugins.is_empty() && (!searching || crate::prefs_search::shows_all(ui)) {
|
||||
if plugins.is_empty() && (!searching || crate::ui_kit::settings_search::shows_all(ui)) {
|
||||
crate::ui_kit::text(ui, crate::i18n::tr("ui.plugins_empty"), crate::ui_kit::Text::Hint("plugins_empty"));
|
||||
}
|
||||
ui.spacing_mut().item_spacing.y = t.space_8;
|
||||
let mut shown = 0usize;
|
||||
for plugin in plugins.iter().filter(|plugin| searching || matches_query(plugin, &query)) {
|
||||
if searching
|
||||
&& !crate::prefs_search::shows_all(ui)
|
||||
&& !crate::prefs_search::texts_hit(ui, &[&plugin.name, &plugin.id, &plugin.description, &plugin.version])
|
||||
&& !crate::ui_kit::settings_search::shows_all(ui)
|
||||
&& !crate::ui_kit::settings_search::texts_hit(ui, &[&plugin.name, &plugin.id, &plugin.description, &plugin.version])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
shown += 1;
|
||||
if searching {
|
||||
crate::prefs_search::reveal(ui);
|
||||
crate::prefs_search::push_force(ui);
|
||||
crate::ui_kit::settings_search::reveal(ui);
|
||||
crate::ui_kit::settings_search::push_force(ui);
|
||||
}
|
||||
plugin_card(app, ui, dir, plugin);
|
||||
if searching {
|
||||
crate::prefs_search::pop_force(ui);
|
||||
crate::ui_kit::settings_search::pop_force(ui);
|
||||
}
|
||||
}
|
||||
if !searching && !plugins.is_empty() && shown == 0 {
|
||||
@@ -257,11 +257,11 @@ fn plugin_card(app: &mut SolveApp, ui: &mut egui::Ui, dir: &Path, plugin: &Plugi
|
||||
}
|
||||
crate::scenario::publish_rect(&format!("prefs:plugin:{}:card", plugin.id), response.rect);
|
||||
let texts = [&plugin.name, &plugin.id, &plugin.description, &plugin.version];
|
||||
if let Some(text) = texts.iter().find(|text| crate::prefs_search::texts_hit(ui, &[*text])) {
|
||||
crate::prefs_search::remember(ui, text, response.rect);
|
||||
if let Some(text) = texts.iter().find(|text| crate::ui_kit::settings_search::texts_hit(ui, &[*text])) {
|
||||
crate::ui_kit::settings_search::remember(ui, text, response.rect);
|
||||
}
|
||||
for text in texts {
|
||||
crate::prefs_search::follow(ui, text, response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, text, response.rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,17 +458,17 @@ mod tests {
|
||||
crate::scenario::clear_handles();
|
||||
let mut output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
|
||||
ui.set_width(646.0);
|
||||
let _search = crate::prefs_search::begin(ui, query);
|
||||
crate::prefs_search::begin_page(ui, crate::prefs::PAGE_PLUGINS, "Extend > Plugins");
|
||||
let _search = crate::ui_kit::settings_search::begin(ui, query);
|
||||
crate::ui_kit::settings_search::begin_page(ui, crate::prefs::PAGE_PLUGINS, "Extend > Plugins");
|
||||
crate::ui_kit::scope(ui, |ui| page_in_dir(&mut h.app, ui, Some(&dir)));
|
||||
crate::prefs_search::store_nav(ui);
|
||||
crate::ui_kit::settings_search::store_nav(ui);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
let (_, _, hits) = crate::prefs_search::nav(&ctx);
|
||||
let (_, _, hits) = crate::ui_kit::settings_search::nav(&ctx);
|
||||
assert_eq!(hits.len(), 1, "{query}: one real plugin result");
|
||||
assert_eq!(hits[0].page, crate::prefs::PAGE_PLUGINS);
|
||||
crate::prefs_search::set_reveal(&ctx, Some(crate::prefs_search::reveal_for(hits[0].page, &hits[0].label)));
|
||||
crate::ui_kit::settings_search::set_reveal(&ctx, Some(crate::ui_kit::settings_search::reveal_for(hits[0].page, &hits[0].label)));
|
||||
page_frame(&mut h.app, &ctx, &dir, vec![]);
|
||||
let card = crate::scenario::rect_of("prefs:plugin:public-one:card").unwrap();
|
||||
assert!(crate::scenario::handle_at("prefs:revealed").unwrap().distance(card.center()) <= 1.0);
|
||||
|
||||
+61
-68
@@ -467,11 +467,11 @@ fn search_header(ui: &mut egui::Ui, app: &mut SolveApp) {
|
||||
}
|
||||
crate::scenario::publish_handle("prefs:search", field.rect.center());
|
||||
if field.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
crate::ui_kit::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
settle_search(app);
|
||||
crate::prefs_search::set_reveal(ui.ctx(), app.prefs_window.reveal.clone());
|
||||
crate::prefs_search::set_force_scroll(ui.ctx(), !app.prefs_window.filter.trim().is_empty());
|
||||
crate::ui_kit::settings_search::set_reveal(ui.ctx(), app.prefs_window.reveal.clone());
|
||||
crate::ui_kit::settings_search::set_force_scroll(ui.ctx(), !app.prefs_window.filter.trim().is_empty());
|
||||
}
|
||||
|
||||
/// Keys for the search field. Shortcut capture keeps the key for itself.
|
||||
@@ -484,7 +484,7 @@ fn drive_search(ui: &mut egui::Ui, app: &mut SolveApp) -> bool {
|
||||
if ctx.input_mut(|input| input.consume_key(egui::Modifiers::COMMAND, egui::Key::F)) {
|
||||
ctx.memory_mut(|memory| memory.request_focus(id));
|
||||
}
|
||||
let sink = crate::prefs_search::key_sink(&ctx);
|
||||
let sink = crate::ui_kit::settings_search::key_sink(&ctx);
|
||||
let mut typed = false;
|
||||
if !sink {
|
||||
let modifiers = ctx.input(|input| input.modifiers);
|
||||
@@ -511,20 +511,20 @@ fn drive_search(ui: &mut egui::Ui, app: &mut SolveApp) -> bool {
|
||||
if query.is_empty() {
|
||||
return typed;
|
||||
}
|
||||
let (nav_query, cursor, hits) = crate::prefs_search::nav(&ctx);
|
||||
let (nav_query, cursor, hits) = crate::ui_kit::settings_search::nav(&ctx);
|
||||
let fresh = nav_query == query;
|
||||
let cursor = if fresh { cursor.min(hits.len().saturating_sub(1)) } else { 0 };
|
||||
if ctx.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown)) && fresh && !hits.is_empty() {
|
||||
crate::prefs_search::set_cursor(&ctx, (cursor + 1).min(hits.len() - 1));
|
||||
crate::ui_kit::settings_search::set_cursor(&ctx, (cursor + 1).min(hits.len() - 1));
|
||||
} else if ctx.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp)) && fresh && !hits.is_empty() {
|
||||
crate::prefs_search::set_cursor(&ctx, cursor.saturating_sub(1));
|
||||
crate::ui_kit::settings_search::set_cursor(&ctx, cursor.saturating_sub(1));
|
||||
} else if fresh && !hits.is_empty() && ctx.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter)) {
|
||||
if let Some(hit) = hits.get(cursor) {
|
||||
open_result(app, hit.page, &hit.label);
|
||||
ctx.memory_mut(|memory| memory.surrender_focus(id));
|
||||
}
|
||||
} else {
|
||||
crate::prefs_search::set_cursor(&ctx, cursor);
|
||||
crate::ui_kit::settings_search::set_cursor(&ctx, cursor);
|
||||
}
|
||||
if !app.prefs_window.filter.trim().is_empty() && ctx.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
|
||||
app.prefs_window.filter.clear();
|
||||
@@ -551,7 +551,7 @@ fn open_result(app: &mut SolveApp, page: usize, label: &str) {
|
||||
app.prefs_window.search_from = None;
|
||||
app.prefs_window.section = page;
|
||||
app.prefs_window.land_on = Some(page);
|
||||
app.prefs_window.reveal = Some(crate::prefs_search::reveal_for(page, label));
|
||||
app.prefs_window.reveal = Some(crate::ui_kit::settings_search::reveal_for(page, label));
|
||||
}
|
||||
|
||||
/// An empty field returns to the page that was up when the search started.
|
||||
@@ -1297,7 +1297,7 @@ preference_settings! {
|
||||
/// on every start (#10138). A saved language keeps winning, so existing users don't change.
|
||||
language: String = "system".into(), 0;
|
||||
/// Shared workspace composition; system windows and popups retain their purpose.
|
||||
layout: crate::shell::LayoutMode = Default::default(), PAGE_LAYOUT;
|
||||
layout: crate::ui_kit::LayoutMode = Default::default(), PAGE_LAYOUT;
|
||||
/// Ordered view controls and their appearance.
|
||||
#[serde(default)]
|
||||
navigation_bar: crate::navigation_bar::NavigationBar = Default::default(), PAGE_LAYOUT;
|
||||
@@ -1802,7 +1802,7 @@ pub struct PrefsWindow {
|
||||
/// Set when a result is chosen, so the page draw does not put the previous tab back.
|
||||
land_on: Option<usize>,
|
||||
/// The row to scroll to after a result is chosen.
|
||||
reveal: Option<crate::prefs_search::Reveal>,
|
||||
reveal: Option<crate::ui_kit::settings_search::Reveal>,
|
||||
/// Where the theme Import tile was drawn last frame: a file dropped there is a theme (#8226).
|
||||
import_tile: Option<egui::Rect>,
|
||||
}
|
||||
@@ -1902,7 +1902,7 @@ struct Snapshot {
|
||||
/// With no record yet, keep the small deferred legacy import state too.
|
||||
panel_layout_deferred: Option<crate::panel_runtime::PanelLayoutPreferences>,
|
||||
dark: bool,
|
||||
layout: crate::shell::LayoutMode,
|
||||
layout: crate::ui_kit::LayoutMode,
|
||||
grid: bool,
|
||||
navigation_bar: bool,
|
||||
tool_cursor: bool,
|
||||
@@ -2525,16 +2525,6 @@ pub(crate) fn pivot_under(app: &SolveApp, proj: &crate::viewport::Proj, at: Pos2
|
||||
if b.is_empty() { app.cam.target } else { b.center() }
|
||||
}
|
||||
|
||||
/// A plain number typed into a field, with a decimal point or a decimal comma (#1347): "2.5"
|
||||
/// and "2,5" are both 2.5, as a numeric keypad types either depending on the locale. Spaces are
|
||||
/// ignored and "−" is a minus, as in egui's own parser. A comma beside a point ("1.234,5") or a
|
||||
/// second comma is not guessed at: the text is not a number.
|
||||
pub fn parse_number(text: &str) -> Option<f64> {
|
||||
let t: String = text.chars().filter(|c| !c.is_whitespace()).map(|c| if c == '\u{2212}' { '-' } else { c }).collect();
|
||||
let t = if !t.contains('.') && t.matches(',').count() == 1 { t.replacen(',', ".", 1) } else { t };
|
||||
t.parse::<f64>().ok().filter(|v| v.is_finite())
|
||||
}
|
||||
|
||||
// Existing callers retain their numeric-editor API; kit owns the widget.
|
||||
|
||||
/// A length typed into a field, in millimetres: a bare number is in the design's units (`scale`
|
||||
@@ -2712,7 +2702,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
app.prefs_window.search_from = None;
|
||||
app.prefs_window.land_on = None;
|
||||
app.prefs_window.reveal = None;
|
||||
crate::prefs_search::set_reveal(ctx, None);
|
||||
crate::ui_kit::settings_search::set_reveal(ctx, None);
|
||||
app.prefs_window.snapshot = None;
|
||||
// A question left up when the dialog closed is not still asked when it opens again.
|
||||
app.space_mouse.restore_pending = false;
|
||||
@@ -2730,8 +2720,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
{
|
||||
app.prefs_window.reveal = None;
|
||||
}
|
||||
let sink = crate::prefs_search::shift_key_sink(ctx);
|
||||
crate::prefs_search::set_key_sink_prev(ctx, sink);
|
||||
let sink = crate::ui_kit::settings_search::shift_key_sink(ctx);
|
||||
crate::ui_kit::settings_search::set_key_sink_prev(ctx, sink);
|
||||
let mut button: Option<&str> = None;
|
||||
let before = app.preferences.clone();
|
||||
let capturing = crate::keymap::capturing(app);
|
||||
@@ -2904,38 +2894,38 @@ fn panel_restore_banner(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
|
||||
fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
panel_restore_banner(app, ui, t);
|
||||
let _search = crate::prefs_search::begin(ui, &app.prefs_window.filter);
|
||||
if crate::prefs_search::querying(ui) {
|
||||
let _search = crate::ui_kit::settings_search::begin(ui, &app.prefs_window.filter);
|
||||
if crate::ui_kit::settings_search::querying(ui) {
|
||||
// Paint only the pages that match. A first pass is invisible, so the 3D mouse rows and the
|
||||
// agent list cannot draw on top of the setting that did match.
|
||||
let mut keep = vec![false; WINDOW_PAGES.len()];
|
||||
for (index, _) in WINDOW_PAGES.iter().enumerate() {
|
||||
crate::prefs_search::begin_page(ui, index, &page_path(index));
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let mut probe = crate::prefs_search::detached_child(ui, ("prefs-search-probe", index), true);
|
||||
crate::ui_kit::settings_search::begin_page(ui, index, &page_path(index));
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
let mut probe = crate::ui_kit::settings_search::detached_child(ui, ("prefs-search-probe", index), true);
|
||||
draw_page(app, &mut probe, index, t);
|
||||
keep[index] = crate::prefs_search::hits(ui) > before;
|
||||
keep[index] = crate::ui_kit::settings_search::hits(ui) > before;
|
||||
}
|
||||
crate::prefs_search::rewind_search_layout(ui);
|
||||
crate::ui_kit::settings_search::rewind_search_layout(ui);
|
||||
for (index, _) in WINDOW_PAGES.iter().enumerate() {
|
||||
if !keep.get(index).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
crate::prefs_search::begin_page(ui, index, &page_path(index));
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let mut child = crate::prefs_search::detached_child(ui, ("prefs-search-page", index), false);
|
||||
crate::ui_kit::settings_search::begin_page(ui, index, &page_path(index));
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
let mut child = crate::ui_kit::settings_search::detached_child(ui, ("prefs-search-page", index), false);
|
||||
draw_page(app, &mut child, index, t);
|
||||
crate::prefs_search::commit_search_child(ui, &child, before);
|
||||
crate::ui_kit::settings_search::commit_search_child(ui, &child, before);
|
||||
}
|
||||
if crate::prefs_search::hits(ui) == 0 {
|
||||
if crate::ui_kit::settings_search::hits(ui) == 0 {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("prefs.search_empty")).size(t.font_body).color(t.form_dim));
|
||||
}
|
||||
} else {
|
||||
let label = page_path(app.prefs_window.section);
|
||||
crate::prefs_search::begin_page(ui, app.prefs_window.section, &label);
|
||||
crate::ui_kit::settings_search::begin_page(ui, app.prefs_window.section, &label);
|
||||
draw_page(app, ui, app.prefs_window.section, t);
|
||||
}
|
||||
crate::prefs_search::store_nav(ui);
|
||||
crate::ui_kit::settings_search::store_nav(ui);
|
||||
}
|
||||
|
||||
fn draw_page(app: &mut SolveApp, ui: &mut egui::Ui, index: usize, t: &Tokens) {
|
||||
@@ -4649,7 +4639,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn navigation_bar_preferences_round_trip_and_recover_hostile_fields() {
|
||||
use crate::navigation_bar::{Edge, Idle, Labels, NavigationBar, Position, Size};
|
||||
use crate::navigation_bar::{Labels, NavigationBar, Position};
|
||||
use crate::ui_kit::{NavigationEdge, NavigationIdle, NavigationSize};
|
||||
use serde_json::json;
|
||||
assert_eq!(super::lenient(&json!({})).navigation_bar, NavigationBar::default());
|
||||
let p = super::lenient(&json!({"navigation_bar": {
|
||||
@@ -4659,7 +4650,7 @@ mod tests {
|
||||
assert_eq!(p.navigation_bar.slots, ["pan", "future_slot", "separator", "separator", "separator", "separator", "fit"]);
|
||||
assert_eq!(
|
||||
(p.navigation_bar.position, p.navigation_bar.edge, p.navigation_bar.size, p.navigation_bar.labels, p.navigation_bar.idle),
|
||||
(Position::Left, Edge::Top, Size::Regular, Labels::Labels, Idle::Hide)
|
||||
(Position::Left, NavigationEdge::Top, NavigationSize::Regular, Labels::Labels, NavigationIdle::Hide)
|
||||
);
|
||||
let saved = serde_json::to_value(&p).unwrap();
|
||||
assert_eq!(serde_json::from_value::<super::Prefs>(saved).unwrap(), p);
|
||||
@@ -4669,10 +4660,10 @@ mod tests {
|
||||
let mut expected = p.clone();
|
||||
match field {
|
||||
"position" => expected.navigation_bar.position = Position::default(),
|
||||
"edge" => expected.navigation_bar.edge = Edge::default(),
|
||||
"size" => expected.navigation_bar.size = Size::default(),
|
||||
"edge" => expected.navigation_bar.edge = NavigationEdge::default(),
|
||||
"size" => expected.navigation_bar.size = NavigationSize::default(),
|
||||
"labels" => expected.navigation_bar.labels = Labels::default(),
|
||||
_ => expected.navigation_bar.idle = Idle::default(),
|
||||
_ => expected.navigation_bar.idle = NavigationIdle::default(),
|
||||
}
|
||||
assert_eq!(super::lenient(&value), expected, "{field}");
|
||||
value["navigation_bar"][field] = json!("unknown");
|
||||
@@ -4736,7 +4727,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn navigation_bar_editor_buttons_and_appearance_update_preferences() {
|
||||
use crate::navigation_bar::{Idle, Labels, MenuDisplay, Position, Size};
|
||||
use crate::navigation_bar::{Labels, MenuDisplay, Position};
|
||||
use crate::ui_kit::{NavigationIdle, NavigationSize};
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1100}));
|
||||
@@ -4753,20 +4745,20 @@ mod tests {
|
||||
assert_eq!(h.app.preferences.navigation_bar.menu_display("visual_style"), MenuDisplay::IconAndValue);
|
||||
navbar_click(&mut h, "prefs:navbar:anchor:0");
|
||||
assert_eq!(h.app.preferences.navigation_bar.position, Position::Left);
|
||||
assert_eq!(h.app.preferences.navigation_bar.edge, crate::navigation_bar::Edge::Top);
|
||||
assert_eq!(h.app.preferences.navigation_bar.edge, crate::ui_kit::NavigationEdge::Top);
|
||||
navbar_click(&mut h, "prefs:navbar:anchor:5");
|
||||
assert_eq!(h.app.preferences.navigation_bar.position, Position::Right);
|
||||
assert_eq!(h.app.preferences.navigation_bar.edge, crate::navigation_bar::Edge::Bottom);
|
||||
assert_eq!(h.app.preferences.navigation_bar.edge, crate::ui_kit::NavigationEdge::Bottom);
|
||||
navbar_click(&mut h, "prefs:navbar:size:0");
|
||||
assert_eq!(h.app.preferences.navigation_bar.size, Size::Compact);
|
||||
assert_eq!(h.app.preferences.navigation_bar.size, NavigationSize::Compact);
|
||||
navbar_click(&mut h, "prefs:navbar:size:1");
|
||||
assert_eq!(h.app.preferences.navigation_bar.size, Size::Regular);
|
||||
assert_eq!(h.app.preferences.navigation_bar.size, NavigationSize::Regular);
|
||||
navbar_click(&mut h, "prefs:navbar:buttons:1");
|
||||
assert_eq!(h.app.preferences.navigation_bar.labels, Labels::Labels);
|
||||
navbar_click(&mut h, "prefs:navbar:idle:0");
|
||||
assert_eq!(h.app.preferences.navigation_bar.idle, Idle::Always);
|
||||
assert_eq!(h.app.preferences.navigation_bar.idle, NavigationIdle::Always);
|
||||
navbar_click(&mut h, "prefs:navbar:idle:2");
|
||||
assert_eq!(h.app.preferences.navigation_bar.idle, Idle::Hide);
|
||||
assert_eq!(h.app.preferences.navigation_bar.idle, NavigationIdle::Hide);
|
||||
let before = h.app.preferences.navigation_bar.clone();
|
||||
navbar_click(&mut h, "prefs:navbar:preview_as:1");
|
||||
assert_eq!(h.app.preferences.navigation_bar, before, "canvas preview is transient");
|
||||
@@ -4982,18 +4974,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn navigation_bar_preview_stage_follows_anchor_live() {
|
||||
use crate::navigation_bar::{Edge, Position};
|
||||
use crate::navigation_bar::Position;
|
||||
use crate::ui_kit::NavigationEdge;
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1100}));
|
||||
h.call("ui.prefs", json!({"open":"Interface/Navigation bar","navigation_bar":{"slots":["pan","fit","display"]}}));
|
||||
for (index, edge, position) in [
|
||||
(0, Edge::Top, Position::Left),
|
||||
(1, Edge::Top, Position::Centre),
|
||||
(2, Edge::Top, Position::Right),
|
||||
(3, Edge::Bottom, Position::Left),
|
||||
(4, Edge::Bottom, Position::Centre),
|
||||
(5, Edge::Bottom, Position::Right),
|
||||
(0, NavigationEdge::Top, Position::Left),
|
||||
(1, NavigationEdge::Top, Position::Centre),
|
||||
(2, NavigationEdge::Top, Position::Right),
|
||||
(3, NavigationEdge::Bottom, Position::Left),
|
||||
(4, NavigationEdge::Bottom, Position::Centre),
|
||||
(5, NavigationEdge::Bottom, Position::Right),
|
||||
] {
|
||||
navbar_click(&mut h, &format!("prefs:navbar:anchor:{index}"));
|
||||
h.frames(3);
|
||||
@@ -5004,11 +4997,11 @@ mod tests {
|
||||
assert_eq!(stage.height(), crate::ui_kit::NAV_STAGE_HEIGHT);
|
||||
let inset = crate::ui_kit::NAV_BORDER + crate::ui_kit::NAV_STAGE_INSET;
|
||||
match edge {
|
||||
Edge::Top => {
|
||||
NavigationEdge::Top => {
|
||||
assert_eq!(bar.top(), stage.top() + inset);
|
||||
assert!(count.top() >= bar.bottom(), "the count sits below a top bar");
|
||||
}
|
||||
Edge::Bottom => {
|
||||
NavigationEdge::Bottom => {
|
||||
assert_eq!(bar.bottom(), stage.bottom() - inset);
|
||||
assert!(count.bottom() <= bar.top(), "the count sits above a bottom bar");
|
||||
}
|
||||
@@ -6597,7 +6590,7 @@ mod tests {
|
||||
app.autosave_minutes = 9.0;
|
||||
app.ui.dark = false;
|
||||
app.prefs_window.follow.launch = Some("theme.json".into());
|
||||
app.ui.layout = crate::shell::LayoutMode::Docked;
|
||||
app.ui.layout = crate::ui_kit::LayoutMode::Docked;
|
||||
app.ui.show_navigation_bar = false;
|
||||
app.ui.tool_cursor = true;
|
||||
app.ui.visual_style = 2;
|
||||
@@ -6612,7 +6605,7 @@ mod tests {
|
||||
assert_eq!(app.autosave_minutes, if page == PAGE_GENERAL { 5.0 } else { 9.0 });
|
||||
assert_eq!(app.ui.dark, page == PAGE_APPEARANCE);
|
||||
assert_eq!(app.prefs_window.follow.launch.is_none(), page == PAGE_APPEARANCE);
|
||||
assert_eq!(app.ui.layout == crate::shell::LayoutMode::Floating, page == PAGE_LAYOUT);
|
||||
assert_eq!(app.ui.layout == crate::ui_kit::LayoutMode::Floating, page == PAGE_LAYOUT);
|
||||
assert_eq!(app.ui.show_navigation_bar, page == PAGE_LAYOUT);
|
||||
assert_eq!(app.ui.tool_cursor, page != PAGE_TOOLS);
|
||||
assert_eq!(app.ui.visual_style, if page == PAGE_GRAPHICS { 0 } else { 2 });
|
||||
@@ -8643,13 +8636,13 @@ mod tests {
|
||||
#[test]
|
||||
fn number_fields_take_a_decimal_point_or_a_decimal_comma() {
|
||||
// #1347
|
||||
assert_eq!(super::parse_number("2.5"), Some(2.5));
|
||||
assert_eq!(super::parse_number("2,5"), Some(2.5));
|
||||
assert_eq!(super::parse_number(" -0,25 "), Some(-0.25));
|
||||
assert_eq!(super::parse_number("\u{2212}1,5"), Some(-1.5));
|
||||
assert_eq!(super::parse_number("12"), Some(12.0));
|
||||
assert_eq!(crate::ui_kit::parse_number("2.5"), Some(2.5));
|
||||
assert_eq!(crate::ui_kit::parse_number("2,5"), Some(2.5));
|
||||
assert_eq!(crate::ui_kit::parse_number(" -0,25 "), Some(-0.25));
|
||||
assert_eq!(crate::ui_kit::parse_number("\u{2212}1,5"), Some(-1.5));
|
||||
assert_eq!(crate::ui_kit::parse_number("12"), Some(12.0));
|
||||
for bad in ["1.234,5", "1,2,3", ",", "abc", "", "inf", "NaN"] {
|
||||
assert_eq!(super::parse_number(bad), None, "{bad}");
|
||||
assert_eq!(crate::ui_kit::parse_number(bad), None, "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let response = setting(app, ui).backing_on_hover_text(app.tr(help));
|
||||
crate::scenario::publish_target(&format!("prefs:{target}"), response.rect);
|
||||
crate::scenario::publish_target(&format!("dialog:{legacy}"), response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr(help));
|
||||
}
|
||||
});
|
||||
@@ -141,7 +141,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let response = setting::offer_stand_up(app, ui).backing_on_hover_text(app.tr("prefs.stand_up_note"));
|
||||
crate::scenario::publish_target("prefs:offer_stand_up", response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr("prefs.stand_up_note"));
|
||||
}
|
||||
});
|
||||
@@ -179,7 +179,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let response = setting::double_click_components(app, ui).backing_on_hover_text(app.tr("prefs.design_tools.double_click_help"));
|
||||
crate::scenario::publish_target("prefs:double_click_components", response.rect);
|
||||
crate::scenario::publish_target("dialog:Double-click a component to activate it, or to edit a linked one in place", response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr("prefs.design_tools.double_click_help"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -434,7 +434,7 @@ fn paint_monitor(p: &egui::Painter, rect: egui::Rect, ink: egui::Color32) {
|
||||
/// shows the rows without it, like every other card.
|
||||
fn computer_card(ui: &mut egui::Ui, add: impl FnOnce(&mut egui::Ui)) {
|
||||
let t = Tokens::get();
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
add(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ pub(super) fn layout(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
|ui| {
|
||||
ui_kit::settings_form(ui, t.form_label_width + t.space_16, |ui| {
|
||||
ui_kit::row(ui, app.tr("ui.panel_layout.panel_style"), |ui| {
|
||||
let mut selected = usize::from(app.ui.layout == crate::shell::LayoutMode::Docked);
|
||||
let mut selected = usize::from(app.ui.layout == crate::ui_kit::LayoutMode::Docked);
|
||||
if ui_kit::segmented(
|
||||
ui,
|
||||
&mut selected,
|
||||
@@ -283,7 +283,7 @@ pub(super) fn layout(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
layout_check(ui, app.tr("prefs.status_bar"), app.tr("prefs.status_bar_note"), &mut app.preferences.status_bar, "status_bar");
|
||||
});
|
||||
ui_kit::LayoutPreview {
|
||||
compact: app.ui.layout == crate::shell::LayoutMode::Docked,
|
||||
compact: app.ui.layout == crate::ui_kit::LayoutMode::Docked,
|
||||
labels: app.preferences.ribbon_labels,
|
||||
history: history_choice(app),
|
||||
translucent: app.preferences.browser_float,
|
||||
@@ -346,7 +346,7 @@ pub(super) fn layout(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
app.tr("prefs.browser"),
|
||||
&options,
|
||||
usize::from(app.preferences.browser_float),
|
||||
app.ui.layout == crate::shell::LayoutMode::Floating,
|
||||
app.ui.layout == crate::ui_kit::LayoutMode::Floating,
|
||||
) {
|
||||
app.preferences.browser_float = selected == 1;
|
||||
}
|
||||
@@ -659,7 +659,7 @@ fn browser_visibility(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_form(ui, 0.0, |ui| {
|
||||
ui_kit::row(ui, app.tr("prefs.browser_shown"), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = Tokens::get().space_6;
|
||||
crate::prefs_search::note_current(ui, app.tr("prefs.layout_page.on_hover"));
|
||||
crate::ui_kit::settings_search::note_current(ui, app.tr("prefs.layout_page.on_hover"));
|
||||
ui.add_enabled_ui(!crate::timeline_left::stacked(app), |ui| {
|
||||
let mut selected = usize::from(app.preferences.browser_show == BrowserShow::Hover);
|
||||
if ui_kit::settings_segments(
|
||||
|
||||
@@ -163,7 +163,7 @@ fn orbit(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let width = ui_kit::ContentWidth::label(ui, label) + Tokens::get().space_16 + Tokens::get().space_4;
|
||||
ui_kit::settings_form(ui, width, |ui| {
|
||||
ui_kit::settings_row_help(ui, label, help, |ui| {
|
||||
crate::prefs_search::note_choices(ui, &orbits.each_ref().map(String::as_str));
|
||||
crate::ui_kit::settings_search::note_choices(ui, &orbits.each_ref().map(String::as_str));
|
||||
let mut selected = usize::from(!app.preferences.space_mouse_model_pivot);
|
||||
let response = ui.scope(|ui| {
|
||||
ui_kit::segmented(
|
||||
@@ -180,11 +180,11 @@ fn orbit(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if response.inner {
|
||||
app.preferences.space_mouse_model_pivot = selected == 0;
|
||||
}
|
||||
if !crate::prefs_search::texts_hit(ui, &[label]) {
|
||||
if !crate::ui_kit::settings_search::texts_hit(ui, &[label]) {
|
||||
for option in &orbits {
|
||||
if let Some(rect) = crate::scenario::rect_of(&format!("dialog:{label}:{option}")) {
|
||||
crate::prefs_search::remember(ui, option, rect);
|
||||
crate::prefs_search::follow(ui, option, rect);
|
||||
crate::ui_kit::settings_search::remember(ui, option, rect);
|
||||
crate::ui_kit::settings_search::follow(ui, option, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Navigation bar editor. The shared viewport painter owns the preview's appearance.
|
||||
use crate::navigation::NavCaps;
|
||||
use crate::navigation_bar::{self, Edge, Idle, Kind, Labels, MenuDisplay, NavigationBar, Position, Size, SlotOptions};
|
||||
use crate::navigation_bar::{self, Labels, MenuDisplay, NavigationBar, Position, SlotOptions};
|
||||
use crate::navigation_bar_view;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::{NavigationEdge, NavigationIdle, NavigationKind, NavigationSize};
|
||||
use crate::{SolveApp, scenario, theme::Tokens, ui_kit};
|
||||
use egui::{Rect, Sense, vec2};
|
||||
use serde_json::json;
|
||||
@@ -63,7 +64,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
});
|
||||
scenario::publish_rect("prefs:navbar:header", header.response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
preview(app, ui, &mut state, width);
|
||||
if state.selected.is_some() {
|
||||
let card = ui_kit::settings_card(ui, |ui| inspector(app, ui, &mut state));
|
||||
@@ -240,8 +241,8 @@ fn slot_count(app: &SolveApp, ui: &mut egui::Ui, stage: Rect, bar: Rect) {
|
||||
Position::Right => stage.right() - ui_kit::NAV_BORDER - ui_kit::NAV_STAGE_INSET - ui_kit::NAV_PADDING - size.x,
|
||||
};
|
||||
let y = match app.preferences.navigation_bar.edge {
|
||||
Edge::Top => bar.bottom() + t.space_8,
|
||||
Edge::Bottom => bar.top() - t.space_8 - size.y,
|
||||
NavigationEdge::Top => bar.bottom() + t.space_8,
|
||||
NavigationEdge::Bottom => bar.top() - t.space_8 - size.y,
|
||||
};
|
||||
let rect = Rect::from_min_size(egui::pos2(x, y), size);
|
||||
ui.painter().with_clip_rect(stage).galley(rect.min, galley, view.text_dim);
|
||||
@@ -283,7 +284,7 @@ fn drop_on_preview(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, tar
|
||||
selected: false,
|
||||
kind: slot.kind,
|
||||
labels: false,
|
||||
menu: slot.kind == Kind::Menu,
|
||||
menu: slot.kind == NavigationKind::Menu,
|
||||
},
|
||||
app.preferences.navigation_bar.size,
|
||||
);
|
||||
@@ -437,7 +438,7 @@ fn available(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(Tokens::get().space_8, Tokens::get().space_8);
|
||||
for slot in navigation_bar::CATALOGUE {
|
||||
if slot.kind == Kind::Separator || app.preferences.navigation_bar.slots.iter().any(|id| id == slot.id) {
|
||||
if slot.kind == NavigationKind::Separator || app.preferences.navigation_bar.slots.iter().any(|id| id == slot.id) {
|
||||
continue;
|
||||
}
|
||||
let dragging = egui::DragAndDrop::payload::<Drag>(ui.ctx()).is_some_and(|payload| payload.from_menu && payload.id.slot == slot.id);
|
||||
@@ -480,9 +481,13 @@ fn option_grid(bar: &mut NavigationBar, ui: &mut egui::Ui, first: usize) {
|
||||
ui.allocate_ui_with_layout(vec2(width.max(0.0), t.control_height), egui::Layout::top_down(egui::Align::Min), |ui| match index {
|
||||
0 => anchor_option(bar, ui),
|
||||
1 => {
|
||||
option(ui, "size", &mut usize::from(bar.size == Size::Regular), &["navbar.custom.compact", "navbar.custom.regular"], |i| {
|
||||
bar.size = if i == 1 { Size::Regular } else { Size::Compact }
|
||||
});
|
||||
option(
|
||||
ui,
|
||||
"size",
|
||||
&mut usize::from(bar.size == NavigationSize::Regular),
|
||||
&["navbar.custom.compact", "navbar.custom.regular"],
|
||||
|i| bar.size = if i == 1 { NavigationSize::Regular } else { NavigationSize::Compact },
|
||||
);
|
||||
}
|
||||
2 => {
|
||||
option(
|
||||
@@ -498,16 +503,16 @@ fn option_grid(bar: &mut NavigationBar, ui: &mut egui::Ui, first: usize) {
|
||||
ui,
|
||||
"idle",
|
||||
&mut match bar.idle {
|
||||
Idle::Always => 0,
|
||||
Idle::Fade => 1,
|
||||
Idle::Hide => 2,
|
||||
NavigationIdle::Always => 0,
|
||||
NavigationIdle::Fade => 1,
|
||||
NavigationIdle::Hide => 2,
|
||||
},
|
||||
&["navbar.custom.always", "navbar.custom.fade", "navbar.custom.hide_idle"],
|
||||
|i| {
|
||||
bar.idle = match i {
|
||||
0 => Idle::Always,
|
||||
2 => Idle::Hide,
|
||||
_ => Idle::Fade,
|
||||
0 => NavigationIdle::Always,
|
||||
2 => NavigationIdle::Hide,
|
||||
_ => NavigationIdle::Fade,
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -520,7 +525,7 @@ fn option_grid(bar: &mut NavigationBar, ui: &mut egui::Ui, first: usize) {
|
||||
}
|
||||
|
||||
fn anchor_option(bar: &mut NavigationBar, ui: &mut egui::Ui) {
|
||||
let selected = usize::from(bar.edge == Edge::Bottom) * 3
|
||||
let selected = usize::from(bar.edge == NavigationEdge::Bottom) * 3
|
||||
+ match bar.position {
|
||||
Position::Left => 0,
|
||||
Position::Centre => 1,
|
||||
@@ -536,7 +541,7 @@ fn anchor_option(bar: &mut NavigationBar, ui: &mut egui::Ui) {
|
||||
]
|
||||
.map(crate::i18n::tr);
|
||||
if let Some(index) = ui_kit::settings_anchor_field(ui, crate::i18n::tr("navbar.custom.position"), selected, &names) {
|
||||
bar.edge = if index >= 3 { Edge::Bottom } else { Edge::Top };
|
||||
bar.edge = if index >= 3 { NavigationEdge::Bottom } else { NavigationEdge::Top };
|
||||
bar.position = match index % 3 {
|
||||
0 => Position::Left,
|
||||
2 => Position::Right,
|
||||
|
||||
@@ -177,7 +177,7 @@ fn orbit(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui_kit::settings_form(ui, width, |ui| {
|
||||
ui_kit::settings_row_help(ui, label, help, |ui| {
|
||||
let options = [crate::i18n::tr("prefs.navigation.turntable"), crate::i18n::tr("prefs.navigation.free")];
|
||||
crate::prefs_search::note_choices(ui, &options);
|
||||
crate::ui_kit::settings_search::note_choices(ui, &options);
|
||||
let mut selected = usize::from(app.preferences.free_orbit);
|
||||
let response = ui.scope(|ui| {
|
||||
ui_kit::segmented(
|
||||
@@ -190,11 +190,11 @@ fn orbit(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
if response.inner {
|
||||
app.preferences.free_orbit = selected == 1;
|
||||
}
|
||||
if !crate::prefs_search::texts_hit(ui, &[label]) {
|
||||
if !crate::ui_kit::settings_search::texts_hit(ui, &[label]) {
|
||||
for option in options {
|
||||
if let Some(rect) = crate::scenario::rect_of(&format!("dialog:{label}:{option}")) {
|
||||
crate::prefs_search::remember(ui, option, rect);
|
||||
crate::prefs_search::follow(ui, option, rect);
|
||||
crate::ui_kit::settings_search::remember(ui, option, rect);
|
||||
crate::ui_kit::settings_search::follow(ui, option, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ fn workspace_selector(app: &mut SolveApp, ui: &mut Ui, view: &LayoutPreferencesV
|
||||
let title = app.tr("ui.panel_layout.layout_workspace").to_owned();
|
||||
ui_kit::settings_form(ui, 0.0, |ui| {
|
||||
ui_kit::sized_row(ui, &title, t.control_height, |ui| {
|
||||
crate::prefs_search::note_choices(ui, &options);
|
||||
crate::ui_kit::settings_search::note_choices(ui, &options);
|
||||
ui.horizontal(|ui| {
|
||||
let reset_label = app.tr("ui.panel_layout.reset_short");
|
||||
let reset_width = ui_kit::ContentWidth::name(ui, reset_label) + t.space_24 + t.space_16;
|
||||
@@ -239,8 +239,8 @@ fn tiles(app: &mut SolveApp, ui: &mut Ui, view: &LayoutPreferencesView, state: &
|
||||
Tile::Save => app.tr("ui.panel_layout.save_as_tile"),
|
||||
})
|
||||
.collect();
|
||||
crate::prefs_search::note_choices(ui, &names);
|
||||
crate::prefs_search::note_choices(
|
||||
crate::ui_kit::settings_search::note_choices(ui, &names);
|
||||
crate::ui_kit::settings_search::note_choices(
|
||||
ui,
|
||||
&[app.tr("ui.panel_layout.save"), app.tr("ui.panel_layout.replace"), app.tr("ui.panel_layout.reset")],
|
||||
);
|
||||
@@ -464,7 +464,7 @@ fn tile_menu(app: &SolveApp, ui: &mut Ui, state: &mut State, tile: &Response, en
|
||||
},
|
||||
);
|
||||
if popup.inner.is_some() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
crate::ui_kit::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,8 +535,8 @@ fn popover(app: &mut SolveApp, ui: &mut Ui, view: &LayoutPreferencesView, state:
|
||||
ui_kit::settings_form(ui, 0.0, |ui| {
|
||||
ui.set_width(width);
|
||||
ui.spacing_mut().item_spacing = vec2(t.space_8, t.space_8);
|
||||
crate::prefs_search::push_force(ui);
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
crate::ui_kit::settings_search::push_force(ui);
|
||||
crate::ui_kit::settings_search::note_key_sink(ui.ctx());
|
||||
match &mut popup.kind {
|
||||
PopoverKind::Save { name, chosen } => {
|
||||
let users: Vec<_> = view.entries.iter().filter(|entry| entry.user).collect();
|
||||
@@ -609,7 +609,7 @@ fn popover(app: &mut SolveApp, ui: &mut Ui, view: &LayoutPreferencesView, state:
|
||||
}
|
||||
}
|
||||
trap_focus(ui, &focus_ids);
|
||||
crate::prefs_search::pop_force(ui);
|
||||
crate::ui_kit::settings_search::pop_force(ui);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ fn snapping(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let controls_width = if available >= figure_width + t.space_14 + labels_width { available - figure_width - t.space_14 } else { available };
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Min).with_main_wrap(true), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = t.space_14;
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(figure_width.min(available), t.space_24 * 2.0 + t.space_16), egui::Sense::hover());
|
||||
ui_kit::paint_sketch_guides(&ui.painter_at(rect), rect);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ fn drawing(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
.map(|labels| labels.iter().map(|id| check_width(ui, app.tr(id), true)).fold(0.0, f32::max));
|
||||
let available = ui.available_width();
|
||||
let gap = t.space_24 + t.space_8;
|
||||
let two_columns = available >= t.space_24 * 20.0 + gap && !crate::prefs_search::suppress_chrome(ui);
|
||||
let two_columns = available >= t.space_24 * 20.0 + gap && !crate::ui_kit::settings_search::suppress_chrome(ui);
|
||||
let left = if two_columns { (available - gap) * natural[0] / (natural[0] + natural[1]).max(1.0) } else { available };
|
||||
let right = if two_columns { available - gap - left } else { available };
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Min).with_main_wrap(true), |ui| {
|
||||
@@ -214,8 +214,8 @@ fn opening(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
);
|
||||
picture_group(ui, app.tr("prefs.sketch_exit_view"), |ui| {
|
||||
let label = app.tr("prefs.sketch_exit_view");
|
||||
let mut indexed = crate::prefs_search::texts_hit(ui, &[label]);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
let mut indexed = crate::ui_kit::settings_search::texts_hit(ui, &[label]);
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::section_heading(ui, label);
|
||||
}
|
||||
let cards = ui.horizontal_wrapped(|ui| {
|
||||
@@ -260,19 +260,19 @@ fn opening(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::scenario::publish_target("dialog:When a sketch ends", response.rect);
|
||||
}
|
||||
let option = app.tr(label_id);
|
||||
if !indexed && crate::prefs_search::texts_hit(ui, &[option]) {
|
||||
crate::prefs_search::remember(ui, option, response.rect);
|
||||
if !indexed && crate::ui_kit::settings_search::texts_hit(ui, &[option]) {
|
||||
crate::ui_kit::settings_search::remember(ui, option, response.rect);
|
||||
indexed = true;
|
||||
}
|
||||
crate::prefs_search::follow(ui, option, response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, option, response.rect);
|
||||
if response.clicked() {
|
||||
app.preferences.sketch_exit_view = choice;
|
||||
}
|
||||
}
|
||||
});
|
||||
crate::scenario::publish_rect("prefs:sketch_exit_view:choices", cards.response.rect);
|
||||
crate::prefs_search::remember(ui, label, cards.response.rect);
|
||||
crate::prefs_search::follow(ui, label, cards.response.rect);
|
||||
crate::ui_kit::settings_search::remember(ui, label, cards.response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, label, cards.response.rect);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -342,8 +342,8 @@ fn selecting(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_section(ui, "selection", app.tr("prefs.selecting"), |ui| {
|
||||
picture_group(ui, app.tr("prefs.sketch.click_selects"), |ui| {
|
||||
let label = app.tr("prefs.sketch.click_selects");
|
||||
let mut indexed = crate::prefs_search::texts_hit(ui, &[label]);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
let mut indexed = crate::ui_kit::settings_search::texts_hit(ui, &[label]);
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::section_heading(ui, label);
|
||||
}
|
||||
let cards = ui.horizontal_wrapped(|ui| {
|
||||
@@ -373,19 +373,19 @@ fn selecting(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::scenario::publish_target("dialog:Click selects whole bodies", response.rect);
|
||||
}
|
||||
let option = app.tr(label);
|
||||
if !indexed && crate::prefs_search::texts_hit(ui, &[option]) {
|
||||
crate::prefs_search::remember(ui, option, response.rect);
|
||||
if !indexed && crate::ui_kit::settings_search::texts_hit(ui, &[option]) {
|
||||
crate::ui_kit::settings_search::remember(ui, option, response.rect);
|
||||
indexed = true;
|
||||
}
|
||||
crate::prefs_search::follow(ui, option, response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, option, response.rect);
|
||||
if response.clicked() {
|
||||
app.ui.pick_bodies = bodies;
|
||||
}
|
||||
}
|
||||
});
|
||||
crate::scenario::publish_rect("prefs:pick_bodies:choices", cards.response.rect);
|
||||
crate::prefs_search::remember(ui, label, cards.response.rect);
|
||||
crate::prefs_search::follow(ui, label, cards.response.rect);
|
||||
crate::ui_kit::settings_search::remember(ui, label, cards.response.rect);
|
||||
crate::ui_kit::settings_search::follow(ui, label, cards.response.rect);
|
||||
});
|
||||
check(
|
||||
ui,
|
||||
@@ -434,8 +434,8 @@ fn selecting(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
|
||||
/// Natural-height picture groups own their search entries without allocating a form label column.
|
||||
fn picture_group(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui)) {
|
||||
let gate = crate::prefs_search::block_gate(ui, label, None);
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let gate = crate::ui_kit::settings_search::block_gate(ui, label, None);
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
let show = gate.as_ref().is_none_or(|gate| gate.show);
|
||||
let content = |ui: &mut egui::Ui| {
|
||||
let group = ui.scope(|ui| ui_kit::named_control(ui, label, add));
|
||||
@@ -444,10 +444,10 @@ fn picture_group(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui)
|
||||
if show {
|
||||
content(ui);
|
||||
} else {
|
||||
crate::prefs_search::run_hidden(ui, label, content);
|
||||
crate::ui_kit::settings_search::run_hidden(ui, label, content);
|
||||
}
|
||||
if let Some(gate) = &gate {
|
||||
crate::prefs_search::block_finish(ui, gate, show, before);
|
||||
crate::ui_kit::settings_search::block_finish(ui, gate, show, before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ fn check(ui: &mut egui::Ui, key: &str, label: &str, help: Option<&str>, value: &
|
||||
let help = help.map(crate::i18n::tr);
|
||||
ui.horizontal_top(|ui| {
|
||||
let t = Tokens::get();
|
||||
let show_help = help.is_some() && !crate::prefs_search::suppress_chrome(ui);
|
||||
let show_help = help.is_some() && !crate::ui_kit::settings_search::suppress_chrome(ui);
|
||||
let width = (ui.available_width() - if show_help { t.space_8 + t.space_12 + 1.0 } else { 0.0 }).max(0.0).min(check_width(
|
||||
ui,
|
||||
crate::i18n::tr(label),
|
||||
|
||||
@@ -130,7 +130,7 @@ pub(super) fn choose(app: &mut SolveApp, night: bool, id: &str) {
|
||||
}
|
||||
|
||||
pub(super) fn row(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
row_contents(app, ui);
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ pub(super) fn row(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
fn row_contents(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let key = ui.make_persistent_id("theme_picker_state");
|
||||
let mut state = ui.data(|d| d.get_temp::<State>(key)).unwrap_or_default();
|
||||
let search = crate::prefs_search::suppress_chrome(ui);
|
||||
let search = crate::ui_kit::settings_search::suppress_chrome(ui);
|
||||
let mut action = None;
|
||||
let mut action_theme = None;
|
||||
let t = Tokens::get();
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let response = setting(app, ui).backing_on_hover_text(app.tr(help));
|
||||
crate::scenario::publish_target(&format!("prefs:{key}"), response.rect);
|
||||
crate::scenario::publish_target(&format!("dialog:{legacy}"), response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr(help));
|
||||
}
|
||||
});
|
||||
@@ -42,7 +42,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let response = setting(app, ui).backing_on_hover_text(app.tr(help));
|
||||
crate::scenario::publish_target(&format!("prefs:{key}"), response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr(help));
|
||||
}
|
||||
});
|
||||
@@ -60,7 +60,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let response = setting::placement_handles(app, ui).backing_on_hover_text(app.tr("ui.placement_handles_tip"));
|
||||
crate::scenario::publish_target("prefs:placement_handles", response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr("ui.placement_handles_tip"));
|
||||
}
|
||||
});
|
||||
@@ -76,7 +76,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let response = setting::assembly_move_handles(app, ui).backing_on_hover_text(app.tr("joint_move.legacy_hint"));
|
||||
crate::scenario::publish_target("prefs:assembly_move_handles", response.rect);
|
||||
crate::scenario::publish_target("dialog:Use joint motion handles for assembly Move", response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, app.tr("joint_move.legacy_hint"));
|
||||
}
|
||||
});
|
||||
@@ -86,7 +86,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let tip = app.tr("prefs.move_drag_highlights_tip");
|
||||
let response = setting::move_drag_highlights(app, ui).backing_on_hover_text(tip);
|
||||
crate::scenario::publish_target("prefs:move_drag_highlights", response.rect);
|
||||
if !crate::prefs_search::suppress_chrome(ui) {
|
||||
if !crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
ui_kit::help_icon(ui, tip);
|
||||
}
|
||||
});
|
||||
@@ -162,12 +162,12 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
/// A single original figure beside a tool's complete setting stack. The columns wrap together
|
||||
/// with their translated labels; search uses the same block gate as other compound preferences.
|
||||
fn tool_group(ui: &mut egui::Ui, label: &str, figure: fn(&egui::Painter, egui::Rect), add: impl FnOnce(&mut egui::Ui)) {
|
||||
let gate = crate::prefs_search::block_gate(ui, label, None);
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let gate = crate::ui_kit::settings_search::block_gate(ui, label, None);
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
if gate.as_ref().is_some_and(|gate| !gate.show) {
|
||||
crate::prefs_search::run_hidden(ui, label, add);
|
||||
crate::ui_kit::settings_search::run_hidden(ui, label, add);
|
||||
if let Some(gate) = &gate {
|
||||
crate::prefs_search::block_finish(ui, gate, false, before);
|
||||
crate::ui_kit::settings_search::block_finish(ui, gate, false, before);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -190,14 +190,14 @@ fn tool_group(ui: &mut egui::Ui, label: &str, figure: fn(&egui::Painter, egui::R
|
||||
ui.allocate_ui_with_layout(egui::vec2(controls_width, 0.0), egui::Layout::top_down(egui::Align::Min), add);
|
||||
});
|
||||
if let Some(gate) = &gate {
|
||||
crate::prefs_search::block_finish(ui, gate, true, before);
|
||||
crate::ui_kit::settings_search::block_finish(ui, gate, true, before);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Create section's card: one border around a row per tool.
|
||||
fn create_card(ui: &mut egui::Ui, rows: impl FnOnce(&mut egui::Ui)) {
|
||||
let t = Tokens::get();
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if crate::ui_kit::settings_search::suppress_chrome(ui) {
|
||||
rows(ui);
|
||||
return;
|
||||
}
|
||||
@@ -215,17 +215,17 @@ fn create_card(ui: &mut egui::Ui, rows: impl FnOnce(&mut egui::Ui)) {
|
||||
/// One tool in the Create card: its icons in tiles and its name on the left, its settings on the
|
||||
/// right, a rule above every row but the first. Search uses the same block gate as `tool_group`.
|
||||
fn create_row(ui: &mut egui::Ui, label: &str, icons: &[&str], first: bool, add: impl FnOnce(&mut egui::Ui)) {
|
||||
let gate = crate::prefs_search::block_gate(ui, label, None);
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let gate = crate::ui_kit::settings_search::block_gate(ui, label, None);
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
if gate.as_ref().is_some_and(|gate| !gate.show) {
|
||||
crate::prefs_search::run_hidden(ui, label, add);
|
||||
crate::ui_kit::settings_search::run_hidden(ui, label, add);
|
||||
if let Some(gate) = &gate {
|
||||
crate::prefs_search::block_finish(ui, gate, false, before);
|
||||
crate::ui_kit::settings_search::block_finish(ui, gate, false, before);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
let chrome = !crate::prefs_search::suppress_chrome(ui);
|
||||
let chrome = !crate::ui_kit::settings_search::suppress_chrome(ui);
|
||||
let margin = egui::Margin::symmetric(t.space_14 as i8, t.space_10 as i8);
|
||||
let row = egui::Frame::NONE.inner_margin(margin).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
@@ -260,6 +260,6 @@ fn create_row(ui: &mut egui::Ui, label: &str, icons: &[&str], first: bool, add:
|
||||
ui.painter().hline(row.response.rect.x_range(), row.response.rect.top(), egui::Stroke::new(1.0, t.dialog_rule));
|
||||
}
|
||||
if let Some(gate) = &gate {
|
||||
crate::prefs_search::block_finish(ui, gate, true, before);
|
||||
crate::ui_kit::settings_search::block_finish(ui, gate, true, before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,8 +395,9 @@ pub(crate) fn preview_scene(scene: &mut ProductScene, preview: Option<&Preview>)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn menu(app: &SolveApp, body: &str) -> crate::context_menu::Item {
|
||||
use crate::context_menu::{Item, cmd};
|
||||
pub(crate) fn menu(app: &SolveApp, body: &str) -> crate::ui_kit::Item {
|
||||
use crate::context_menu::cmd;
|
||||
use crate::ui_kit::Item;
|
||||
let selected_face = app
|
||||
.render_workspace
|
||||
.view
|
||||
@@ -529,8 +530,8 @@ pub(crate) fn selection_bar(app: &mut SolveApp, ui: &mut egui::Ui, free: egui::R
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let anchor = egui::pos2(
|
||||
crate::shell::within(screen.center().x, free.left(), free.right()),
|
||||
crate::shell::within(screen.top() - crate::theme::Tokens::get().space_12, free.top(), free.bottom()),
|
||||
crate::ui_kit::within(screen.center().x, free.left(), free.right()),
|
||||
crate::ui_kit::within(screen.top() - crate::theme::Tokens::get().space_12, free.top(), free.bottom()),
|
||||
);
|
||||
let ctx = ui.ctx().clone();
|
||||
let pill = ui_kit::Container::selection_card(&ctx, egui::Id::unique("render_selection"), anchor, free, |ui| {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub(crate) fn focused(app: &SolveApp) -> bool {
|
||||
}
|
||||
|
||||
pub(crate) fn compact(app: &SolveApp, width: f32) -> bool {
|
||||
width < 1100.0 || app.ui.layout == crate::shell::LayoutMode::Docked
|
||||
width < 1100.0 || app.ui.layout == crate::ui_kit::LayoutMode::Docked
|
||||
}
|
||||
|
||||
pub(crate) fn focus_shortcut(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
|
||||
@@ -3,9 +3,9 @@ use super::{ImageMethod, RenderSelection, cpu, hdr, inspector, setups, studio};
|
||||
use crate::{
|
||||
SolveApp,
|
||||
browser::{self, Row},
|
||||
context_menu::{self, Item},
|
||||
context_menu,
|
||||
theme::Tokens,
|
||||
ui_kit,
|
||||
ui_kit::{self, Item},
|
||||
};
|
||||
use egui::Color32;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -2757,7 +2757,7 @@ fn preview_error(app: &mut SolveApp, ui: &mut egui::Ui, rect: egui::Rect, error:
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(bounded), |ui| {
|
||||
ui.set_clip_rect(ui.clip_rect().intersect(bounded));
|
||||
ui.set_max_width(bounded.width().min(t.form_card_width * 2.0));
|
||||
crate::ui_kit::Panel::feature_frame(ui.ctx(), crate::shell::LayoutMode::Floating, crate::frame::Side::Left).inner_margin(t.space_8).show(
|
||||
crate::ui_kit::Panel::feature_frame(ui.ctx(), crate::ui_kit::LayoutMode::Floating, crate::frame::Side::Left).inner_margin(t.space_8).show(
|
||||
ui,
|
||||
|ui| {
|
||||
crate::ui_kit::text(ui, error, crate::ui_kit::Text::Error("render.preview_failure"));
|
||||
|
||||
@@ -259,7 +259,7 @@ fn render_workspace_shot_lens_saves_only_lens_and_orthographic_keeps_faded_contr
|
||||
fn inspector_dispatch_has_one_editor_per_selection_independent_of_every_stage() {
|
||||
use super::super::{RenderSelection, Stage};
|
||||
for dark in [false, true] {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = fixture();
|
||||
h.app.preferences.theme = if dark { "dark" } else { "light" }.into();
|
||||
h.app.ui.layout = layout;
|
||||
@@ -313,7 +313,7 @@ fn inspector_dispatch_has_one_editor_per_selection_independent_of_every_stage()
|
||||
assert!(h.app.render_workspace.view.job.is_none());
|
||||
assert!(h.app.render_workspace.view.completed.is_none());
|
||||
}
|
||||
if !dark && layout == crate::shell::LayoutMode::Floating {
|
||||
if !dark && layout == crate::ui_kit::LayoutMode::Floating {
|
||||
appearance_inspector_capture(&mut h, &format!("dispatch-{kind}"));
|
||||
}
|
||||
}
|
||||
@@ -514,12 +514,12 @@ fn stage_catalogs_fill_a_290_by_530_panel_and_keep_two_line_labels_inside_tiles(
|
||||
crate::theme::set_dark(dark);
|
||||
h.ctx.set_visuals(if dark { egui::Visuals::dark() } else { egui::Visuals::light() });
|
||||
// A resizable 290 pt card needs the floating, wide-screen policy.
|
||||
h.app.ui.layout = crate::shell::LayoutMode::Floating;
|
||||
h.app.ui.layout = crate::ui_kit::LayoutMode::Floating;
|
||||
h.app.preferences.render_browser_width = 290.0;
|
||||
h.app.preferences.render_browser_side = "left".into();
|
||||
super::super::select_image_method(&mut h.app, method);
|
||||
library::open_category(&mut h.app, kind);
|
||||
crate::theme::TabAccent::publish(&h.app, &h.ctx);
|
||||
h.app.publish_tab_accent(&h.ctx);
|
||||
let document = h.app.session.doc.to_json();
|
||||
let history = (h.app.session.undo.len(), h.app.session.redo.len());
|
||||
// Panel::workspace reserves an 8 pt bottom gap in these bounds.
|
||||
@@ -1032,7 +1032,7 @@ fn ribbon_navigation_reuses_inspector_and_preserves_exact_unsaved_intent() {
|
||||
#[test]
|
||||
fn render_stage_bar_keeps_actions_and_personal_preferences_in_both_layouts() {
|
||||
assert!(crate::workspace::panels("RENDER").is_empty());
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = fixture();
|
||||
h.app.ui.layout = layout;
|
||||
h.app.toolbar_custom.panels.insert("DESIGN/RENDER/RESULT".into(), vec!["render.export".into()]);
|
||||
@@ -1299,7 +1299,7 @@ fn wear_selection_precedes_output_page_and_appearance_selection_chrome() {
|
||||
"inspector title has missing, empty or out-of-header bounds: {context}, bounds={bounds:?}"
|
||||
);
|
||||
}
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = fixture();
|
||||
h.app.ui.layout = layout;
|
||||
h.app.session.selection = vec![solidcraft_engine::Sel::Body { name: "Subject".into() }];
|
||||
|
||||
@@ -4915,7 +4915,7 @@ fn render_workspace_stage_chips_dispatch_without_mutations_and_remember_each_doc
|
||||
|
||||
#[test]
|
||||
fn render_workspace_stage_shell_degrades_without_overlap_at_small_window_sizes() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
enter(&mut h.app, true).unwrap();
|
||||
h.app.ui.layout = mode;
|
||||
@@ -5342,7 +5342,7 @@ fn render_workspace_shots_toggle_persists_and_design_roundtrip_preserves_state()
|
||||
|
||||
#[test]
|
||||
fn render_workspace_canvas_controls_and_fit_respect_panels_and_shots_in_both_layouts() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = layout;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1000}));
|
||||
@@ -5427,7 +5427,7 @@ fn assert_product_fits_canvas(app: &SolveApp) {
|
||||
|
||||
#[test]
|
||||
fn render_workspace_hosts_float_and_shield_input_in_both_ribbon_layouts() {
|
||||
for (layout, page) in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked]
|
||||
for (layout, page) in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked]
|
||||
.into_iter()
|
||||
.flat_map(|layout| [inspector::Editor::Scene, inspector::Editor::Appearance].map(|page| (layout, page)))
|
||||
{
|
||||
@@ -5516,7 +5516,7 @@ fn render_workspace_hosts_float_and_shield_input_in_both_ribbon_layouts() {
|
||||
|
||||
#[test]
|
||||
fn render_workspace_assignment_footer_fits_stacked_card_with_shots_shown_and_hidden() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = layout;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1000}));
|
||||
@@ -5539,7 +5539,7 @@ fn render_workspace_assignment_footer_fits_stacked_card_with_shots_shown_and_hid
|
||||
|
||||
#[test]
|
||||
fn render_workspace_card_grips_swap_sides_and_resize_without_design_edits() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = layout;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1000}));
|
||||
@@ -5559,7 +5559,7 @@ fn render_workspace_card_grips_swap_sides_and_resize_without_design_edits() {
|
||||
h.step(&json!({"drag":[[rail.x,rail.y],[rail.x+50.0,rail.y]],"steps":8})).unwrap();
|
||||
h.frames(3);
|
||||
// A compact dock keeps its minimum width and leaves the remembered width alone.
|
||||
if layout == crate::shell::LayoutMode::Docked {
|
||||
if layout == crate::ui_kit::LayoutMode::Docked {
|
||||
assert_eq!(h.app.preferences.render_inspector_width, width);
|
||||
assert_eq!(h.app.shell.workspace_panels["render:inspector"].0.width(), card_width);
|
||||
} else {
|
||||
@@ -5985,7 +5985,7 @@ fn render_workspace_browser_look_lens_counts_distinct_objects_and_selects_them()
|
||||
|
||||
#[test]
|
||||
fn render_workspace_dock_command_swaps_sides_without_design_edits() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = layout;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1000}));
|
||||
@@ -6027,7 +6027,7 @@ fn render_workspace_dock_command_swaps_sides_without_design_edits() {
|
||||
|
||||
#[test]
|
||||
fn render_workspace_dock_grip_menus_dispatch_the_other_side() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = layout;
|
||||
h.call("ui.resize", json!({"width":1600,"height":1000}));
|
||||
@@ -6601,7 +6601,7 @@ fn render_workspace_shots_heading_matches_design_history_typography_and_strip_ba
|
||||
}
|
||||
let mut h = Harness::new();
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap();
|
||||
h.app.ui.layout = crate::shell::LayoutMode::Docked;
|
||||
h.app.ui.layout = crate::ui_kit::LayoutMode::Docked;
|
||||
h.app.preferences.timeline_left = false;
|
||||
h.app.ui.show_timeline = true;
|
||||
h.frames(3);
|
||||
@@ -11392,7 +11392,7 @@ fn cpu_general_glass_path_availability_uses_reviewed_unsaved_saved_optics_withou
|
||||
fn render_ui_at_world_points_hit_the_painted_surface_and_letterbox_does_not_select() {
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_engine::Sel;
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
for side in ["left", "right"] {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("solid.box", json!({"corner":[-10,-10,-10],"length":20,"width":20,"height":20,"body_name":"Pick target"})).unwrap();
|
||||
@@ -11471,7 +11471,7 @@ fn render_ui_at_world_points_hit_the_painted_surface_and_letterbox_does_not_sele
|
||||
|
||||
#[test]
|
||||
fn rendered_camera_layout_cache_and_fit_share_the_exact_frame_without_moving_outer_controls() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for side in ["left", "right"] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = mode;
|
||||
@@ -11672,7 +11672,7 @@ fn light_stage_describes_world_coordinates_without_inert_frame_settings_or_edits
|
||||
let preview = light::preview(&h.app).unwrap();
|
||||
assert!(!lights.is_empty());
|
||||
let lights = serde_json::to_value(lights).unwrap();
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
h.app.ui.layout = layout;
|
||||
for width in [800.0, 1440.0] {
|
||||
let output = draw(&mut h, width, vec![]);
|
||||
|
||||
@@ -143,7 +143,7 @@ fn plastic_rule_editor_contract() {
|
||||
|
||||
#[test]
|
||||
fn sheet_rule_editor_delete_reloads_and_plastic_has_no_delete() {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let mut h = scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.call("ui.set", json!({"layout":layout}));
|
||||
|
||||
@@ -53,76 +53,17 @@ use solidcraft_engine::Session;
|
||||
use crate::control::{ControlRequest, Outcome, handle};
|
||||
use crate::{Services, SolveApp};
|
||||
|
||||
// The widget registry (handles, rects and counts drawn this frame) lives in its own crate so UI
|
||||
// components can publish without depending on the app; re-exported here for the scenario runner.
|
||||
pub use solidcraft_ui_registry::{
|
||||
clear_handles, count_of, handle_at, handles, publish_count, publish_handle, publish_rect, publish_target, publish_target_as, rect_of, rects,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
/// Drag handles drawn this frame (name, screen point): manipulators publish theirs so
|
||||
/// scenarios (and the control channel's `ui.at {handle}`) can grab them.
|
||||
static HANDLES: RefCell<Vec<(String, egui::Pos2)>> = const { RefCell::new(Vec::new()) };
|
||||
/// Painted parts with no widget of their own (the toolbar, the workspace switcher…): rects
|
||||
/// and counts by name, for checks.
|
||||
static RECTS: RefCell<Vec<(String, egui::Rect)>> = const { RefCell::new(Vec::new()) };
|
||||
static COUNTS: RefCell<Vec<(String, f64)>> = const { RefCell::new(Vec::new()) };
|
||||
/// Widths a scenario's `same_width` checks remembered (by rect name).
|
||||
static WIDTHS: RefCell<Vec<(String, f32)>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// A manipulator's handle drawn this frame at `at` (screen), by name: "arrow" (a dialog's value
|
||||
/// arrow), "triad_x"… Call it from the code that draws the handle, every frame it shows.
|
||||
pub fn publish_handle(name: &str, at: egui::Pos2) {
|
||||
// The latest wins (lookups search from the end), so publishing is cheap for browser rows.
|
||||
HANDLES.with(|h| h.borrow_mut().push((name.to_string(), at)));
|
||||
}
|
||||
|
||||
/// Publish a rectangle and its centered interaction handle with the same explicit identity.
|
||||
pub(crate) fn publish_target(name: &str, rect: egui::Rect) {
|
||||
publish_target_as(name, name, rect);
|
||||
}
|
||||
|
||||
/// Publish centered geometry with explicit, independently named handle and rectangle aliases.
|
||||
pub(crate) fn publish_target_as(handle: &str, rect_name: &str, rect: egui::Rect) {
|
||||
publish_handle(handle, rect.center());
|
||||
publish_rect(rect_name, rect);
|
||||
}
|
||||
|
||||
/// Where a handle was drawn last frame.
|
||||
pub fn handle_at(name: &str) -> Option<egui::Pos2> {
|
||||
HANDLES.with(|h| h.borrow().iter().rfind(|(n, _)| n == name).map(|x| x.1))
|
||||
}
|
||||
|
||||
/// Every handle drawn last frame whose name starts with `prefix`.
|
||||
pub fn handles(prefix: &str) -> Vec<(String, egui::Pos2)> {
|
||||
HANDLES.with(|h| h.borrow().iter().filter(|(n, _)| n.starts_with(prefix)).cloned().collect())
|
||||
}
|
||||
|
||||
/// A painted part's rect this frame, by name ("toolbar", "workspace_switcher").
|
||||
pub fn publish_rect(name: &str, r: egui::Rect) {
|
||||
RECTS.with(|h| h.borrow_mut().push((name.to_string(), r)));
|
||||
}
|
||||
|
||||
pub fn rect_of(name: &str) -> Option<egui::Rect> {
|
||||
RECTS.with(|h| h.borrow().iter().rfind(|(n, _)| n == name).map(|x| x.1))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn rects(prefix: &str) -> Vec<(String, egui::Rect)> {
|
||||
RECTS.with(|r| r.borrow().iter().filter(|(name, _)| name.starts_with(prefix)).cloned().collect())
|
||||
}
|
||||
|
||||
/// A number about this frame's drawing, by name ("axis_view_highlights": the axis view's highlighted tips).
|
||||
pub fn publish_count(name: &str, v: f64) {
|
||||
COUNTS.with(|h| h.borrow_mut().push((name.to_string(), v)));
|
||||
}
|
||||
|
||||
pub fn count_of(name: &str) -> Option<f64> {
|
||||
COUNTS.with(|h| h.borrow().iter().rfind(|(n, _)| n == name).map(|x| x.1))
|
||||
}
|
||||
|
||||
/// Forget last frame's handles, rects and counts (called as a frame starts).
|
||||
pub fn clear_handles() {
|
||||
HANDLES.with(|h| h.borrow_mut().clear());
|
||||
RECTS.with(|h| h.borrow_mut().clear());
|
||||
COUNTS.with(|h| h.borrow_mut().clear());
|
||||
}
|
||||
|
||||
/// `v` with "$DIR" in its strings replaced by `dir`.
|
||||
fn with_dir(v: Value, dir: &str) -> Value {
|
||||
match v {
|
||||
@@ -671,7 +612,7 @@ impl Harness {
|
||||
eprintln!("widgets: {texts:?}");
|
||||
}
|
||||
if s["debug"] == json!("handles") {
|
||||
let names: Vec<String> = HANDLES.with(|h| h.borrow().iter().map(|(n, p)| format!("{n} @{:.0},{:.0}", p.x, p.y)).collect());
|
||||
let names: Vec<String> = handles("").iter().map(|(n, p)| format!("{n} @{:.0},{:.0}", p.x, p.y)).collect();
|
||||
eprintln!("handles: {names:?}");
|
||||
}
|
||||
return Ok(());
|
||||
|
||||
@@ -630,7 +630,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let mut picked = None;
|
||||
let screen = ctx.content_rect();
|
||||
let margins = crate::ui_kit::Menu::frame().total_margin().sum();
|
||||
let width = crate::context_menu::LIST_W.min(screen.width());
|
||||
let width = crate::ui_kit::LIST_W.min(screen.width());
|
||||
let w = (width - margins.x).max(0.0);
|
||||
let header = 20.0;
|
||||
let gap = vec2(12.0, 12.0);
|
||||
|
||||
@@ -664,7 +664,7 @@ pub(crate) fn section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Searching for "profiles" must keep this entry on the results page; without a hit here the
|
||||
// whole section was dropped and the name field could not be reached with a query active.
|
||||
let entry = [app.tr("prefs.general.profile_name"), app.tr("prefs.general.save_new"), app.tr("prefs.general.import")];
|
||||
if crate::prefs_search::visible(ui, &entry) {
|
||||
if crate::ui_kit::settings_search::visible(ui, &entry) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let mut draft = std::mem::take(&mut app.settings_profiles.draft);
|
||||
let enter = ui_kit::named_control(ui, app.tr("prefs.general.profile_name"), |ui| {
|
||||
@@ -759,7 +759,7 @@ pub(crate) fn rename_popup(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
return;
|
||||
}
|
||||
let Some(mut rename) = app.settings_profiles.rename.take() else { return };
|
||||
crate::prefs_search::note_key_sink(ctx);
|
||||
crate::ui_kit::settings_search::note_key_sink(ctx);
|
||||
let mut done = None;
|
||||
let popup = ui_kit::Container::new(ctx, ui_kit::Surface::Popup)
|
||||
.area(egui::Id::unique("sc_profile_rename"))
|
||||
|
||||
@@ -1,32 +1,8 @@
|
||||
//! Transient shell geometry shared by automation, drawing and viewport occlusion.
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use egui::{Rect, pos2, vec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::ui_kit::{Container, LayoutMode, SHELL_GAP, Surface, within};
|
||||
use egui::{Rect, pos2};
|
||||
|
||||
use crate::{SolveApp, theme::Tokens, toolbar};
|
||||
|
||||
/// Saved workspace layout; automation may override it for a session.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LayoutMode {
|
||||
#[default]
|
||||
Floating,
|
||||
#[serde(rename = "compact", alias = "docked")]
|
||||
Docked,
|
||||
}
|
||||
|
||||
impl LayoutMode {
|
||||
pub(crate) fn publish(self, ctx: &egui::Context) {
|
||||
ctx.data_mut(|data| data.insert_temp(egui::Id::unique("sc_container_layout"), self));
|
||||
}
|
||||
|
||||
pub(crate) fn current(ctx: &egui::Context) -> Self {
|
||||
ctx.data(|data| data.get_temp(egui::Id::unique("sc_container_layout"))).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The gap from the viewport edge to the floating cards and between the cards.
|
||||
pub(crate) const SHELL_GAP: f32 = 7.0;
|
||||
use crate::{SolveApp, theme::Tokens};
|
||||
|
||||
/// The viewport's top corners are rounded concentrically with the cards: the card margin plus the
|
||||
/// card radius.
|
||||
@@ -62,32 +38,6 @@ pub(crate) fn paint_top_corners(painter: &egui::Painter, viewport: Rect, color:
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Cards {
|
||||
pub workspace: Rect,
|
||||
pub ribbon: Rect,
|
||||
pub bottom: f32,
|
||||
}
|
||||
|
||||
impl Cards {
|
||||
pub fn new(available: Rect, mode: LayoutMode, ribbon_height: f32) -> Self {
|
||||
let margin = Container::for_layout(mode, Surface::Chrome).gap();
|
||||
let gap = margin;
|
||||
let left = available.left() + margin;
|
||||
let top = available.top() + margin;
|
||||
let right = (available.right() - margin).max(left);
|
||||
let workspace = Rect::from_min_size(
|
||||
pos2(left, top),
|
||||
vec2(
|
||||
toolbar::WORKSPACE_WIDTH.min((right - left).max(0.0)),
|
||||
ribbon_height - if !Container::for_layout(mode, Surface::Chrome).floating() { 1.0 } else { 0.0 },
|
||||
),
|
||||
);
|
||||
let ribbon = Rect::from_min_max(pos2((workspace.right() + gap).min(right), top), pos2(right, top + ribbon_height));
|
||||
Self { workspace, ribbon, bottom: ribbon.bottom() + margin }
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell panels measured after drawing the full-bleed viewport. No design or preferences data.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct State {
|
||||
@@ -255,12 +205,6 @@ pub(crate) fn content_rect(app: &SolveApp, viewport: Rect) -> Rect {
|
||||
if content.is_positive() { content } else { viewport }
|
||||
}
|
||||
|
||||
/// `v` kept within `lo..=hi`, or `lo` when that range is empty: `f32::clamp` panics on
|
||||
/// `lo > hi`, which a rect squeezed to nothing between cards gives.
|
||||
pub(crate) fn within(v: f32, lo: f32, hi: f32) -> f32 {
|
||||
if lo <= hi { v.clamp(lo, hi) } else { lo }
|
||||
}
|
||||
|
||||
/// Free anchor space shared by both layouts. Compact edge hosts have already consumed
|
||||
/// their allocation; content-hugging Browser and floating overlays reserve their measured span.
|
||||
pub(crate) fn workspace_content(app: &SolveApp, mut content: Rect) -> Rect {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::scenario::{Harness, rect_of};
|
||||
use crate::toolbar;
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::panels::ActionSink as _;
|
||||
|
||||
@@ -2530,7 +2531,7 @@ fn status_bar_mouse_mapping_opens_the_mappings_and_picks_one_as_the_preferences_
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
// An unknown mapping changes nothing.
|
||||
let before = serde_json::to_value(&h.app.preferences).unwrap();
|
||||
let hostile = crate::context_menu::Item::action("ui.navPreset", "Nope", "").with(json!({"preset": "nope"}));
|
||||
let hostile = crate::ui_kit::Item::action("ui.navPreset", "Nope", "").with(json!({"preset": "nope"}));
|
||||
crate::context_menu::run_item(&mut h.app, &hostile, egui::pos2(0.0, 0.0));
|
||||
assert_eq!(serde_json::to_value(&h.app.preferences).unwrap(), before);
|
||||
// A second click on the button closes the menu it opened.
|
||||
|
||||
@@ -614,10 +614,10 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Mod, doc: &Document) -> bool {
|
||||
// The size in millimetres follows the factor and the other way round; the aspect is kept.
|
||||
if let Some((_, size)) = frame {
|
||||
for (label, natural) in [("Width", size[0]), ("Height", size[1])] {
|
||||
let k = crate::prefs::parse_number(factor.trim()).filter(|k| *k > 0.0).unwrap_or(1.0);
|
||||
let k = crate::ui_kit::parse_number(factor.trim()).filter(|k| *k > 0.0).unwrap_or(1.0);
|
||||
let mut shown = format!("{} mm", crate::sketch_transform_ui::plain(natural * k));
|
||||
if number(ui, label, &mut shown)
|
||||
&& let Some(mm) = crate::prefs::parse_number(shown.trim_end_matches("mm").trim())
|
||||
&& let Some(mm) = crate::ui_kit::parse_number(shown.trim_end_matches("mm").trim())
|
||||
&& mm > 0.0
|
||||
&& natural > 1e-9
|
||||
{
|
||||
|
||||
@@ -820,7 +820,7 @@ mod tests {
|
||||
/// most visibly docked against the window's edge (#9457).
|
||||
#[test]
|
||||
fn pane_stays_still_while_the_pointer_moves_at_fractional_scale() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for (scale, width, height) in [(1.0, 1600.0, 1000.0), (1.25, 1536.0, 921.6), (1.25, 1522.4, 904.8), (1.5, 1280.0, 800.0)] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.ui.layout = layout;
|
||||
@@ -878,7 +878,7 @@ mod tests {
|
||||
/// it sits at the left is not squeezed to nothing beside it.
|
||||
#[test]
|
||||
fn the_header_drags_the_palette_and_it_keeps_its_place() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
h.call("ui.resize", json!({"width":1400,"height":900}));
|
||||
|
||||
@@ -101,7 +101,7 @@ pub struct Offer {
|
||||
/// enable switch (`app.space_mouse.device`, `app.preferences`); the readers present
|
||||
/// (`space_mouse.drivers`); `ctx.input` focus, time and pointer position; `app.status`, `computing_since`,
|
||||
/// `file_picker_busy`, `file_load.busy`, `prefs_window.open`, `space_mouse.calibrating`,
|
||||
/// `issue_report::cad_input_blocked`.
|
||||
/// `ui_kit::input_blocked`.
|
||||
/// - Writes, only its own: `space_mouse.offer_since`, `space_mouse.offer_hidden`;
|
||||
/// `space_mouse_offered` and `space_mouse_offer_never` (preferences) when the user closes it (×),
|
||||
/// or clicks Calibrate or Never.
|
||||
@@ -183,7 +183,7 @@ fn input_blocked(app: &SolveApp, ctx: &egui::Context) -> bool {
|
||||
!ctx.input(|i| i.focused)
|
||||
|| app.file_picker_busy()
|
||||
|| app.file_load.busy()
|
||||
|| crate::issue_report::cad_input_blocked()
|
||||
|| crate::ui_kit::input_blocked()
|
||||
|| app.prefs_window.open
|
||||
|| app.space_mouse.calibrating.is_some()
|
||||
|| app.home.open
|
||||
@@ -463,10 +463,10 @@ mod preferences_tests {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.space_mouse.device = Some("hid 046d:c62b".into());
|
||||
crate::issue_report::set_cad_input_blocked_for_test(true);
|
||||
crate::ui_kit::set_input_blocked(true);
|
||||
h.frames(6);
|
||||
let hidden = h.app.space_mouse.offer_since.is_none() && crate::scenario::rect_of("toast:space_mouse_offer").is_none();
|
||||
crate::issue_report::set_cad_input_blocked_for_test(false);
|
||||
crate::ui_kit::set_input_blocked(false);
|
||||
assert!(hidden, "not drawn while input is blocked");
|
||||
assert!(h.app.space_mouse.calibrating.is_none() && h.app.preferences.space_mouse_offered.is_empty());
|
||||
// And it is there again once the report is gone.
|
||||
@@ -726,10 +726,10 @@ mod preferences_tests {
|
||||
// Where the button was: once the guard hides the card, a name finds nothing (or another
|
||||
// control), so a click by name would not test the click behind it.
|
||||
let at = h.widget(widget).unwrap();
|
||||
crate::issue_report::set_cad_input_blocked_for_test(true);
|
||||
crate::ui_kit::set_input_blocked(true);
|
||||
h.step(&serde_json::json!({"click": at})).unwrap();
|
||||
h.frames(3);
|
||||
crate::issue_report::set_cad_input_blocked_for_test(false);
|
||||
crate::ui_kit::set_input_blocked(false);
|
||||
assert!(h.app.space_mouse.calibrating.is_none() && !h.app.prefs_window.open, "{widget}: no wizard, no Preferences");
|
||||
assert_eq!(h.app.preferences, before, "{widget}: nothing recorded or changed");
|
||||
// And the card is back once the guard is gone, untouched.
|
||||
@@ -768,7 +768,7 @@ mod preferences_tests {
|
||||
let cases: [Stale; 7] = [
|
||||
("the device changed", |h| h.app.space_mouse.device = Some("hid 256f:c63a".into())),
|
||||
("the device went", |h| h.app.space_mouse.device = None),
|
||||
("an issue report took the input", |_| crate::issue_report::set_cad_input_blocked_for_test(true)),
|
||||
("an issue report took the input", |_| crate::ui_kit::set_input_blocked(true)),
|
||||
("Preferences opened", |h| h.app.prefs_window.open = true),
|
||||
("a status toast is up", |h| h.app.status = Some(("Done".into(), crate::now_ms(), false))),
|
||||
("the device was dismissed elsewhere", |h| {
|
||||
@@ -791,7 +791,7 @@ mod preferences_tests {
|
||||
h.app.space_mouse.offer_hidden.clone(),
|
||||
);
|
||||
act(&mut h.app, &h.ctx, &offer, action);
|
||||
crate::issue_report::set_cad_input_blocked_for_test(false);
|
||||
crate::ui_kit::set_input_blocked(false);
|
||||
assert_eq!(h.app.preferences, before, "{action:?}, {what}: nothing recorded or changed");
|
||||
assert_eq!(h.app.space_mouse.calibrating.is_some(), calibrating, "{action:?}, {what}: no wizard");
|
||||
assert_eq!(h.app.prefs_window.open, window, "{action:?}, {what}: Preferences untouched");
|
||||
|
||||
+28
-29
@@ -20,7 +20,7 @@ thread_local! {
|
||||
/// must not change one another's palette while a frame or icon texture is being built.
|
||||
static DARK: Cell<bool> = const { Cell::new(true) };
|
||||
/// The colour vision mode, per thread. Each app
|
||||
/// sets it from its own preferences at the start of every frame (`SolveApp::logic`), on the
|
||||
/// sets it from its own preferences at the start of every frame (in its frame logic), on the
|
||||
/// thread that draws it. Tests run many apps side by side on their own threads in one process:
|
||||
/// with one global, a test in a mode changed the colours under another one
|
||||
/// (`hover_stays_off_during_spin_and_animated_views` failed that way). No worker thread reads
|
||||
@@ -580,7 +580,11 @@ pub(crate) fn scoped_tokens(tokens: Tokens) -> impl Drop {
|
||||
}
|
||||
|
||||
/// Temporarily bind a panel's paint palette without publishing a theme change.
|
||||
pub(crate) fn scoped_panel_tokens(tokens: Tokens) -> impl Drop {
|
||||
///
|
||||
/// `Tokens::get_in` returns these tokens first until the returned guard drops, which restores
|
||||
/// the previous override. A host uses it to draw any component in another palette or accent
|
||||
/// (for example another workspace's) without editing the component.
|
||||
pub fn scoped_panel_tokens(tokens: Tokens) -> impl Drop {
|
||||
struct Restore {
|
||||
previous: Option<Tokens>,
|
||||
// The override belongs to this UI thread, so its guard must not move to another one.
|
||||
@@ -791,11 +795,13 @@ impl TabAccent {
|
||||
ctx.data(|d| d.get_temp::<Option<&'static str>>(egui::Id::unique("sc_tab_icon_accent"))).flatten()
|
||||
}
|
||||
|
||||
pub fn publish(app: &crate::SolveApp, ctx: &egui::Context) {
|
||||
/// Publish the accent of `tab` for this frame. `sketch_open` is whether a sketch is being
|
||||
/// edited, `tool_active` whether a tool or dialog is open; both only steer focus painting.
|
||||
pub fn publish(tab: &str, sketch_open: bool, tool_active: bool, ctx: &egui::Context) {
|
||||
let now = ctx.input(|i| i.time);
|
||||
let (published, accent, fading, style_surfaces) = ctx.data_mut(|data| {
|
||||
let state = data.get_temp_mut_or_default::<TabThemeState>(egui::Id::unique(TAB_STATE));
|
||||
let (published, accent, fading) = state.publish(&app.ui.tab, now);
|
||||
let (published, accent, fading) = state.publish(tab, now);
|
||||
let style_surfaces = published.tab_surfaces || state.styled_surfaces;
|
||||
state.styled_surfaces = published.tab_surfaces;
|
||||
(published, accent, fading, style_surfaces)
|
||||
@@ -818,9 +824,9 @@ impl TabAccent {
|
||||
}
|
||||
ctx.data_mut(|d| {
|
||||
d.insert_temp(egui::Id::unique("sc_tab_accent"), accent);
|
||||
d.insert_temp(egui::Id::unique("sc_tab_icon_accent"), icon_accent_for_tab(&app.ui.tab));
|
||||
d.insert_temp(egui::Id::unique("sc_sketch_open"), app.session.active_sketch.is_some());
|
||||
d.insert_temp(egui::Id::unique("sc_tool_accent"), app.tool.is_some() || app.dialog.is_some());
|
||||
d.insert_temp(egui::Id::unique("sc_tab_icon_accent"), icon_accent_for_tab(tab));
|
||||
d.insert_temp(egui::Id::unique("sc_sketch_open"), sketch_open);
|
||||
d.insert_temp(egui::Id::unique("sc_tool_accent"), tool_active);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1958,15 +1964,13 @@ mod tests {
|
||||
#[test]
|
||||
fn tabs_publish_requests_repaints_only_during_fade_and_zero_is_instant() {
|
||||
use crate::theme_files::OptionValue;
|
||||
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
let ctx = crate::fonts::test_context();
|
||||
let mut state = tabs_state("dark", BTreeMap::new());
|
||||
state.publish("MODEL", 0.0);
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique(TAB_STATE), state));
|
||||
app.ui.tab = "SKETCH".into();
|
||||
let frame = |time| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { time: Some(time), ..Default::default() }, |ui| {
|
||||
TabAccent::publish(&app, ui.ctx());
|
||||
TabAccent::publish("SKETCH", false, false, ui.ctx());
|
||||
crate::gpu::paint_background(ui.painter(), ui.max_rect(), &Tokens::get());
|
||||
});
|
||||
let colors: Vec<_> = out
|
||||
@@ -2175,7 +2179,7 @@ mod tests {
|
||||
themes.extend(crate::theme_files::builtin().iter().map(|theme| (theme.name.as_str(), crate::theme_files::resolve(theme))));
|
||||
let mut failures = Vec::new();
|
||||
for (name, tokens) in themes {
|
||||
for tab in crate::workspace::TABS.iter().copied().chain(["SKETCH", "MESH", "FORM", "SHEET METAL", "RENDER"]) {
|
||||
for tab in super::TAB_NAMES.iter().copied() {
|
||||
let accent = super::TabAccent::for_tab(&tokens, tab);
|
||||
let ratio = super::contrast_ratio(accent.ink, accent.soft);
|
||||
if ratio < 4.5 {
|
||||
@@ -2209,20 +2213,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tab_accent_context_is_per_app_and_does_not_change_widget_visuals() {
|
||||
let mut harness = crate::scenario::Harness::new();
|
||||
let app = &mut harness.app;
|
||||
let first = crate::fonts::test_context();
|
||||
let second = crate::fonts::test_context();
|
||||
let before = first.style_of(egui::Theme::Dark).visuals.clone();
|
||||
app.ui.tab = "SURFACE".into();
|
||||
super::TabAccent::publish(app, &first);
|
||||
super::TabAccent::publish("SURFACE", false, false, &first);
|
||||
assert_eq!(super::TabAccent::current(&first).color, super::Tokens::get().surface_fill);
|
||||
assert_eq!(super::TabAccent::current(&second).color, super::Tokens::get().accent);
|
||||
assert_eq!(super::TabAccent::current_icon_accent(&first), Some("surface"));
|
||||
assert_eq!(super::TabAccent::current_icon_accent(&second), None);
|
||||
assert_eq!(first.style_of(egui::Theme::Dark).visuals, before, "dialog OK, checkboxes and status styles must not inherit the tab");
|
||||
app.ui.tab = "MODEL".into();
|
||||
super::TabAccent::publish(app, &first);
|
||||
super::TabAccent::publish("MODEL", false, false, &first);
|
||||
assert_eq!(super::TabAccent::current(&first).soft, super::Tokens::get().accent_soft);
|
||||
assert_eq!(super::TabAccent::current_icon_accent(&first), None);
|
||||
}
|
||||
@@ -2232,7 +2232,6 @@ mod tests {
|
||||
use crate::theme_files::{OptionValue, ThemeFile};
|
||||
for base in ["dark", "light"] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
let file = crate::theme_files::tab_fixture(base);
|
||||
set_dark(file.base.is_dark());
|
||||
set_vision(ColorVision::Off);
|
||||
@@ -2247,19 +2246,19 @@ mod tests {
|
||||
});
|
||||
set_custom(custom)
|
||||
};
|
||||
let publish = |app: &crate::SolveApp, time| {
|
||||
ctx.run_ui(egui::RawInput { time: Some(time), ..Default::default() }, |ui| TabAccent::publish(app, ui.ctx())).textures_delta.clear();
|
||||
let publish = |tab: &str, time| {
|
||||
ctx.run_ui(egui::RawInput { time: Some(time), ..Default::default() }, |ui| TabAccent::publish(tab, false, false, ui.ctx()))
|
||||
.textures_delta
|
||||
.clear();
|
||||
};
|
||||
prepare(&file, Some(true));
|
||||
apply(&ctx);
|
||||
let before = ctx.global_style().visuals.clone();
|
||||
let base_custom = CUSTOM.get();
|
||||
app.ui.tab = "MODEL".into();
|
||||
publish(&app, 0.0);
|
||||
publish("MODEL", 0.0);
|
||||
assert_eq!(ctx.global_style().visuals, before);
|
||||
app.ui.tab = "SKETCH".into();
|
||||
publish(&app, 1.0);
|
||||
publish(&app, 1.12);
|
||||
publish("SKETCH", 1.0);
|
||||
publish("SKETCH", 1.12);
|
||||
let panel = Tokens::get().panel;
|
||||
assert_ne!(panel, before.panel_fill, "exercise an actual animated panel fill");
|
||||
let mut animated = before.clone();
|
||||
@@ -2268,24 +2267,24 @@ mod tests {
|
||||
assert_eq!(ctx.global_style().visuals, animated, "all non-surface widget/selection/status visuals stay unchanged");
|
||||
// Repair either owned fill independently, even when the panel already matches.
|
||||
ctx.global_style_mut(|style| style.visuals.window_fill = before.window_fill);
|
||||
publish(&app, 1.12);
|
||||
publish("SKETCH", 1.12);
|
||||
assert_eq!(ctx.global_style().visuals, animated);
|
||||
|
||||
assert!(!prepare(&file, Some(false)), "toggling surfaces does not change the base Custom tokens");
|
||||
assert_eq!(CUSTOM.get(), base_custom);
|
||||
// Deliberately omit apply(): a full restyle would conceal stale animated fills.
|
||||
publish(&app, 1.13);
|
||||
publish("SKETCH", 1.13);
|
||||
assert_eq!(ctx.global_style().visuals, before, "turning surfaces off must restore both base fills");
|
||||
|
||||
assert!(!prepare(&file, Some(true)));
|
||||
publish(&app, 2.0);
|
||||
publish("SKETCH", 2.0);
|
||||
assert_ne!(ctx.global_style().visuals.panel_fill, before.panel_fill);
|
||||
let mut plain = file.clone();
|
||||
plain.tabs.clear();
|
||||
plain.options.clear();
|
||||
assert!(!prepare(&plain, None), "a no-option theme can retain the exact same base tokens");
|
||||
assert_eq!(CUSTOM.get(), base_custom);
|
||||
publish(&app, 2.1);
|
||||
publish("SKETCH", 2.1);
|
||||
assert_eq!(ctx.global_style().visuals, before, "restoring a no-option theme must release the surface fills");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ use solidcraft_engine::doc::{
|
||||
use solidcraft_engine::schema::edit::PlaybackAction;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::context_menu::Item;
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::Item;
|
||||
|
||||
/// Apply transport actions from the current marker; endpoints stay bounded, including an
|
||||
/// empty design. All orientations reach the same rollback command.
|
||||
@@ -52,14 +52,14 @@ pub(crate) fn transport_response(app: &mut SolveApp, control: crate::ui_kit::His
|
||||
|
||||
/// The History controls' menu: shows or hides the Play button, and — when the design marks any
|
||||
/// component out of the bar — shows or hides those components' entries (#10393).
|
||||
pub(crate) fn transport_items(app: &SolveApp) -> Vec<crate::context_menu::Item> {
|
||||
pub(crate) fn transport_items(app: &SolveApp) -> Vec<crate::ui_kit::Item> {
|
||||
let show = !app.preferences.history_play_button;
|
||||
let label = crate::i18n::tr(if show { "ui.history_play_button_show" } else { "ui.history_play_button_hide" });
|
||||
let mut items = vec![crate::context_menu::Item::action("ui.historyPlayButton", label, "timeline_play").with(json!({ "show": show }))];
|
||||
let mut items = vec![crate::ui_kit::Item::action("ui.historyPlayButton", label, "timeline_play").with(json!({ "show": show }))];
|
||||
if app.session.doc.components.iter().any(|c| c.history_hidden) {
|
||||
let show_hidden = !app.tree.history_show_hidden;
|
||||
let label = crate::i18n::tr(if show_hidden { "ui.history_show_hidden" } else { "ui.history_hide_hidden" });
|
||||
items.push(crate::context_menu::Item::action("ui.historyShowHidden", label, "eye").with(json!({ "show": show_hidden })));
|
||||
items.push(crate::ui_kit::Item::action("ui.historyShowHidden", label, "eye").with(json!({ "show": show_hidden })));
|
||||
}
|
||||
items
|
||||
}
|
||||
@@ -1397,7 +1397,7 @@ mod tests {
|
||||
assert!(handle_at("timeline:CopyBox").is_none(), "the hidden copy leaves the bar");
|
||||
assert!(handle_at("timeline:PartBox").is_some(), "the rest of the design stays");
|
||||
// The History controls' menu shows them anyway, and hides them again (#10393).
|
||||
fn toggle(h: &Harness) -> context_menu::Item {
|
||||
fn toggle(h: &Harness) -> crate::ui_kit::Item {
|
||||
let items = context_menu::items(&h.app, &Target::HistoryControls);
|
||||
items.into_iter().find(|i| i.id == "ui.historyShowHidden").expect("the bar offers to show hidden entries").clone()
|
||||
}
|
||||
@@ -1710,7 +1710,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn playback_shows_each_feature_in_every_history_layout() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for (dock, width) in
|
||||
[("bottom", 130.0), ("left", 56.0), ("left", crate::ui_kit::history_width(true)), ("left", 320.0), ("under_browser", 170.0)]
|
||||
{
|
||||
@@ -2725,13 +2725,13 @@ mod tests {
|
||||
#[test]
|
||||
fn both_strips_keep_selection_reorder_marker_and_transport() {
|
||||
use crate::scenario::{handle_at, rect_of};
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = two_boxes(false);
|
||||
h.app.ui.layout = mode;
|
||||
h.settle();
|
||||
let viewport = h.app.viewport.rect.unwrap();
|
||||
if mode == crate::shell::LayoutMode::Floating {
|
||||
assert_eq!(rect_of("history:card").unwrap().bottom(), viewport.bottom() - crate::shell::SHELL_GAP);
|
||||
if mode == crate::ui_kit::LayoutMode::Floating {
|
||||
assert_eq!(rect_of("history:card").unwrap().bottom(), viewport.bottom() - crate::ui_kit::SHELL_GAP);
|
||||
} else {
|
||||
let strip = rect_of("history:strip").unwrap();
|
||||
assert_eq!(strip.top(), viewport.bottom());
|
||||
|
||||
@@ -463,7 +463,7 @@ pub(crate) fn dock_drag(app: &mut SolveApp, ui: &egui::Ui, drag: &egui::Response
|
||||
}
|
||||
Dock::Left => {
|
||||
let mut rect = available;
|
||||
let gap = if Container::for_layout(app.ui.layout, Surface::Panel).floating() { crate::shell::SHELL_GAP } else { 0.0 };
|
||||
let gap = if Container::for_layout(app.ui.layout, Surface::Panel).floating() { crate::ui_kit::SHELL_GAP } else { 0.0 };
|
||||
rect.min.x += gap;
|
||||
rect.max.x = (rect.left() + strip_width(app)).min(available.right() - gap);
|
||||
rect.max.y -= gap;
|
||||
@@ -1726,7 +1726,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn docking_previews_match_all_three_destinations_and_stacked_collapse_shares_height() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.ui.layout = mode;
|
||||
@@ -1766,7 +1766,7 @@ mod tests {
|
||||
"{mode:?} {dock}: preview={preview:?} card={card:?}"
|
||||
);
|
||||
assert_eq!(h.app.session.doc.to_json(), design);
|
||||
if mode == crate::shell::LayoutMode::Floating {
|
||||
if mode == crate::ui_kit::LayoutMode::Floating {
|
||||
assert_eq!(h.app.viewport.rect, projection);
|
||||
}
|
||||
}
|
||||
@@ -1806,13 +1806,13 @@ mod tests {
|
||||
h.frames(5);
|
||||
assert!(h.app.preferences.timeline_left_browser_collapsed && h.app.preferences.timeline_collapsed);
|
||||
assert!((h.app.shell.browser.unwrap().top() - browser.top()).abs() < 1.0);
|
||||
assert!(h.app.shell.history.unwrap().bottom() < browser.top() + header_outer_height() * 2.0 + crate::shell::SHELL_GAP + 1.0);
|
||||
assert!(h.app.shell.history.unwrap().bottom() < browser.top() + header_outer_height() * 2.0 + crate::ui_kit::SHELL_GAP + 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folding_cards_follow_their_bounds_through_the_last_animation_frame() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.ui.layout = mode;
|
||||
@@ -1846,7 +1846,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn all_four_transport_buttons_move_to_their_named_positions_in_every_layout() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for (dock, width) in
|
||||
[("bottom", 130.0), ("left", 56.0), ("left", crate::ui_kit::history_width(true)), ("left", 320.0), ("under_browser", 170.0)]
|
||||
{
|
||||
@@ -1868,7 +1868,7 @@ mod tests {
|
||||
for control in crate::ui_kit::HistoryTransport::ALL {
|
||||
let button = crate::scenario::rect_of(&format!("history:{}", control.id())).unwrap();
|
||||
let header = crate::scenario::rect_of(if dock == "bottom" {
|
||||
if mode == crate::shell::LayoutMode::Floating { "history:card" } else { "history:strip" }
|
||||
if mode == crate::ui_kit::LayoutMode::Floating { "history:card" } else { "history:strip" }
|
||||
} else {
|
||||
"history:header"
|
||||
})
|
||||
@@ -1952,7 +1952,7 @@ mod tests {
|
||||
let previous = crate::theme::is_dark();
|
||||
for dark in [false, true] {
|
||||
crate::theme::set_dark(dark);
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for names in [false, true] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.dark = dark;
|
||||
@@ -1981,7 +1981,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rendered_strip_content_never_exceeds_the_viewport_during_resize() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for width in [70.0, 100.0, 130.0, 200.0] {
|
||||
for drag_width in [None, Some(60.0), Some(112.0), Some(160.0)] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
@@ -2058,7 +2058,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn history_is_left_of_the_browser_in_both_layouts_widths_and_browser_states() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for names in [false, true] {
|
||||
for collapsed in [false, true] {
|
||||
let mut h = Harness::new();
|
||||
@@ -2138,7 +2138,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_seam_between_browser_and_history_drags_their_split_and_is_remembered() {
|
||||
// #7371: under the browser, History took exactly half the column; the seam now moves it.
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.ui.layout = mode;
|
||||
@@ -2198,7 +2198,7 @@ mod tests {
|
||||
(false, false, true, opaque),
|
||||
] {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.layout = if floating { crate::shell::LayoutMode::Floating } else { crate::shell::LayoutMode::Docked };
|
||||
h.app.ui.layout = if floating { crate::ui_kit::LayoutMode::Floating } else { crate::ui_kit::LayoutMode::Docked };
|
||||
h.app.ui.show_timeline = true;
|
||||
h.app.preferences.timeline_left = true;
|
||||
h.app.preferences.timeline_under_browser = under_browser;
|
||||
@@ -2210,7 +2210,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn floating_browser_sits_over_the_workspace_and_docks_again() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut harness = Harness::new();
|
||||
harness.app.ui.layout = mode;
|
||||
harness.app.preferences.browser_float = true;
|
||||
|
||||
@@ -759,7 +759,7 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Tokens::get().hover;
|
||||
let item_if = |ui: &mut egui::Ui, label: &str, key: &str, icon: &str, enabled: bool| {
|
||||
crate::context_menu::list_row(ui, &crate::context_menu::Item::action(label, label, icon).key(key).on_if(enabled)).is_some()
|
||||
crate::context_menu::list_row(ui, &crate::ui_kit::Item::action(label, label, icon).key(key).on_if(enabled)).is_some()
|
||||
};
|
||||
let item = |ui: &mut egui::Ui, label: &str, key: &str, icon: &str| item_if(ui, label, key, icon, true);
|
||||
// Insert, Save and Export act on the design, which Home hides.
|
||||
@@ -1520,15 +1520,15 @@ fn dropdown_arrow(painter: &egui::Painter, at: egui::Pos2, ink: Color32) {
|
||||
crate::ui_kit::dropdown_indicator(painter, Rect::from_center_size(at, vec2(10.0, 10.0)), ink);
|
||||
}
|
||||
|
||||
pub(crate) fn shell_layout(app: &SolveApp) -> crate::shell::LayoutMode {
|
||||
if crate::render_workspace::active(app) { crate::shell::LayoutMode::Floating } else { app.ui.layout }
|
||||
pub(crate) fn shell_layout(app: &SolveApp) -> crate::ui_kit::LayoutMode {
|
||||
if crate::render_workspace::active(app) { crate::ui_kit::LayoutMode::Floating } else { app.ui.layout }
|
||||
}
|
||||
|
||||
pub(crate) fn shell_cards(app: &SolveApp, available: Rect) -> crate::shell::Cards {
|
||||
pub(crate) fn shell_cards(app: &SolveApp, available: Rect) -> crate::ui_kit::Cards {
|
||||
if crate::render_workspace::active(app) {
|
||||
crate::ui_kit::StageBar::cards(available)
|
||||
} else {
|
||||
crate::shell::Cards::new(available, shell_layout(app), reserved_height(app))
|
||||
crate::ui_kit::Cards::new(available, shell_layout(app), reserved_height(app), WORKSPACE_WIDTH)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1549,7 +1549,7 @@ pub(crate) fn reserved_height(app: &SolveApp) -> f32 {
|
||||
#[derive(Clone, Copy)]
|
||||
struct RibbonShape {
|
||||
floating: bool,
|
||||
cards: crate::shell::Cards,
|
||||
cards: crate::ui_kit::Cards,
|
||||
r: Rect,
|
||||
ws: Rect,
|
||||
folded: bool,
|
||||
@@ -1599,7 +1599,7 @@ fn open_ribbon(app: &mut SolveApp, ui: &mut egui::Ui, available: Rect, folded: R
|
||||
let layout = shell_layout(app);
|
||||
let floating = Container::for_layout(layout, Surface::Panel).floating();
|
||||
let full = ribbon_height(app.preferences.ribbon_labels);
|
||||
let cards = crate::shell::Cards::new(available, layout, full);
|
||||
let cards = crate::ui_kit::Cards::new(available, layout, full, WORKSPACE_WIDTH);
|
||||
let r = if floating {
|
||||
Rect::from_min_max(cards.workspace.min, cards.ribbon.max)
|
||||
} else {
|
||||
@@ -4841,7 +4841,7 @@ mod dropdown_tests {
|
||||
/// reached with the arrow keys and comes into view.
|
||||
#[test]
|
||||
fn a_tall_drop_down_scrolls_inside_a_short_window() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
@@ -5125,12 +5125,12 @@ mod dropdown_tests {
|
||||
/// button's own right-click menu takes it off again, with both options on.
|
||||
#[test]
|
||||
fn a_pinned_button_is_removed_from_its_own_menu() {
|
||||
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
a_pinned_button_is_removed_in(mode);
|
||||
}
|
||||
}
|
||||
|
||||
fn a_pinned_button_is_removed_in(mode: crate::shell::LayoutMode) {
|
||||
fn a_pinned_button_is_removed_in(mode: crate::ui_kit::LayoutMode) {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.call("ui.set", json!({"layout":mode}));
|
||||
@@ -5452,7 +5452,7 @@ mod dropdown_toggle_tests {
|
||||
assert_eq!(h.app.ui.tab, "SURFACE");
|
||||
assert!(h.app.ui.ribbon_open, "{layout}: the tab's panels open");
|
||||
let open = rect_of("ribbon:open").unwrap();
|
||||
assert!((open.height() - whole.height()).abs() <= 2.0 * crate::shell::SHELL_GAP + 1.0, "{layout}: {open:?} vs {whole:?}");
|
||||
assert!((open.height() - whole.height()).abs() <= 2.0 * crate::ui_kit::SHELL_GAP + 1.0, "{layout}: {open:?} vs {whole:?}");
|
||||
assert!(rect_of("panel:SURFACE:CREATE").is_some(), "{layout}: the panels are drawn");
|
||||
if layout == "compact" {
|
||||
let top = h.app.viewport.rect.unwrap().top();
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::collections::BTreeMap;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{CommandSpec, command_specs};
|
||||
|
||||
use crate::context_menu::Item;
|
||||
use crate::ui_kit::Item;
|
||||
use crate::{SolveApp, ui_kit};
|
||||
|
||||
/// The only workspace so far.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Actions elements for dense cards; see `docs/ui-kit.md` for tokens and usage.
|
||||
use super::metrics::*;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{fonts::RichTextExt as _, scenario, theme::Tokens};
|
||||
use crate::{fonts::RichTextExt as _, theme::Tokens};
|
||||
use egui::{Response, RichText, Ui, vec2};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -182,7 +182,7 @@ pub(crate) fn text_button(ui: &mut Ui, label: &str, enabled: bool) -> Response {
|
||||
enabled,
|
||||
egui::Button::new(RichText::new(label).size(t.font_label).color(t.form_ink)).frame(false).min_size(vec2(0.0, t.control_height)),
|
||||
);
|
||||
scenario::publish_target(&format!("dialog:action:{label}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:action:{label}"), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -241,15 +241,15 @@ pub(super) fn footer_with_left(
|
||||
t.dialog_footer,
|
||||
);
|
||||
t.secondary_hline(ui.painter(), band.x_range(), band.top(), t.dialog_rule);
|
||||
scenario::publish_rect("dialog:footer", band);
|
||||
scenario::publish_count("dialog:ok:enabled", f64::from(ok_enabled && ui.is_enabled()));
|
||||
solidcraft_ui_registry::publish_rect("dialog:footer", band);
|
||||
solidcraft_ui_registry::publish_count("dialog:ok:enabled", f64::from(ok_enabled && ui.is_enabled()));
|
||||
let inner = band.shrink2(vec2(t.space_12, t.space_10));
|
||||
scenario::publish_rect("dialog:footer:actions", inner);
|
||||
solidcraft_ui_registry::publish_rect("dialog:footer:actions", inner);
|
||||
let mut action = if ui.is_enabled() { keyboard(ui.ctx(), ok_enabled) } else { Action::None };
|
||||
let look = ActionLook::new(ok_enabled, !nothing_to_apply, selected, danger);
|
||||
if apply_label.is_some() {
|
||||
let live = if confirm { !nothing_to_apply } else { look.live(Action::Apply) };
|
||||
scenario::publish_count("dialog:apply:enabled", f64::from(live && ui.is_enabled()));
|
||||
solidcraft_ui_registry::publish_count("dialog:apply:enabled", f64::from(live && ui.is_enabled()));
|
||||
}
|
||||
let mut actions_left = inner.right();
|
||||
let row = ui.scope_builder(egui::UiBuilder::new().max_rect(inner).layout(egui::Layout::left_to_right(egui::Align::Center)), |ui| {
|
||||
@@ -299,8 +299,8 @@ pub(super) fn footer_with_left(
|
||||
ui.add(button)
|
||||
});
|
||||
pointer_on_hover(ui, &response);
|
||||
scenario::publish_target("dialog:apply", response.rect);
|
||||
scenario::publish_handle(&format!("dialog:{label}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_target("dialog:apply", response.rect);
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{label}"), response.rect.center());
|
||||
if response.clicked() {
|
||||
action = Action::Apply;
|
||||
}
|
||||
@@ -379,8 +379,8 @@ fn action_button(ui: &mut Ui, label: &str, kind: Action, look: &ActionLook, padd
|
||||
Action::Apply => "dialog:apply",
|
||||
_ => "dialog:cancel",
|
||||
};
|
||||
scenario::publish_target(key, response.rect);
|
||||
scenario::publish_handle(&format!("dialog:{label}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_target(key, response.rect);
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{label}"), response.rect.center());
|
||||
response
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ impl<'a> HeaderActions<'a> {
|
||||
/// Draws the buttons; returns what was clicked, or what Enter and Esc ask for.
|
||||
pub(crate) fn show(&self, ui: &mut Ui, ok_enabled: bool) -> Action {
|
||||
let ok_enabled = ok_enabled && self.rect(Action::Ok).is_some();
|
||||
scenario::publish_count("dialog:ok:enabled", f64::from(ok_enabled && ui.is_enabled()));
|
||||
solidcraft_ui_registry::publish_count("dialog:ok:enabled", f64::from(ok_enabled && ui.is_enabled()));
|
||||
let mut action = if ui.is_enabled() { keyboard(ui.ctx(), ok_enabled) } else { Action::None };
|
||||
let look = ActionLook::new(ok_enabled, true, None, false);
|
||||
let padding = Tokens::get().space_8;
|
||||
@@ -442,7 +442,7 @@ impl<'a> HeaderActions<'a> {
|
||||
}
|
||||
}
|
||||
if let Some(all) = self.slots.iter().map(|(_, _, rect)| *rect).reduce(|a, b| a.union(b)) {
|
||||
scenario::publish_rect("dialog:header:actions", all);
|
||||
solidcraft_ui_registry::publish_rect("dialog:header:actions", all);
|
||||
}
|
||||
action
|
||||
}
|
||||
@@ -500,7 +500,7 @@ pub(crate) fn button_pair(ui: &mut egui::Ui, key: &str, states: [bool; 2], butto
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), on, label));
|
||||
t.button_bevel(ui, &response, on);
|
||||
pointer_on_hover(ui, &response);
|
||||
crate::scenario::publish_target(&format!("{key}:{}", button.key), rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{key}:{}", button.key), rect);
|
||||
if let Some(value) = clicked.get_mut(index) {
|
||||
*value = response.backing_on_hover_text(label).clicked();
|
||||
}
|
||||
@@ -572,8 +572,8 @@ fn button_widget(label: egui::WidgetText, options: Button) -> impl egui::Widget
|
||||
pub fn button(ui: &mut Ui, label: impl Into<egui::WidgetText>, options: Button) -> Response {
|
||||
let label = label.into();
|
||||
let key = label.text().to_owned();
|
||||
if crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[&key]) {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
if super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[&key]) {
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
let response = match options {
|
||||
Button::Enabled(enabled) if super::settings_page_active(ui) => {
|
||||
@@ -621,7 +621,7 @@ pub(crate) fn button_at(ui: &mut Ui, id: &str, rect: egui::Rect, label: impl Int
|
||||
/// Accent action outside the footer, using the same shared button component.
|
||||
pub(crate) fn primary_button(ui: &mut Ui, label: &str) -> Response {
|
||||
let response = button(ui, crate::i18n::tr_label(label), Button::Primary);
|
||||
scenario::publish_target(&format!("dialog:button:{label}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:button:{label}"), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -649,13 +649,13 @@ mod tests;
|
||||
// Additional identities must never replace an existing field handle in compound rows.
|
||||
fn publish_button(ui: &Ui, response: &Response, label: &str) {
|
||||
let key = format!("dialog:button:{}:{label}", super::layout::label(ui));
|
||||
scenario::publish_target(&key, response.rect);
|
||||
solidcraft_ui_registry::publish_target(&key, response.rect);
|
||||
}
|
||||
|
||||
/// Nested action menu in a dense table. Tokens: scope control/font tokens.
|
||||
pub(crate) fn action_menu<R>(ui: &mut Ui, label: &str, add: impl FnOnce(&mut Ui) -> R) -> egui::InnerResponse<Option<R>> {
|
||||
let response = super::Menu::button(ui, crate::i18n::tr_label(label), |ui| super::scope(ui, add));
|
||||
scenario::publish_target(&format!("dialog:menu:{label}"), response.response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:menu:{label}"), response.response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -747,7 +747,7 @@ pub(crate) fn selection_actions(ui: &mut Ui, name: &str, actions: &[super::Stage
|
||||
let (rect, name_response) = ui.allocate_exact_size(vec2(width, t.control_height - t.space_2), egui::Sense::click());
|
||||
name_response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), name));
|
||||
super::text::paint_text(ui, rect.shrink2(vec2(t.space_6, 0.0)), name, font, accent.ink, false);
|
||||
scenario::publish_target("selection:name", rect);
|
||||
solidcraft_ui_registry::publish_target("selection:name", rect);
|
||||
let mut clicked = name_response.clicked().then_some(actions.len());
|
||||
for (index, action) in actions.iter().enumerate() {
|
||||
let response = ui
|
||||
@@ -756,7 +756,7 @@ pub(crate) fn selection_actions(ui: &mut Ui, name: &str, actions: &[super::Stage
|
||||
icon_button_colored(ui, rect, action.icon.unwrap_or("inspect"), action.label, action.primary.then_some(accent.ink))
|
||||
})
|
||||
.inner;
|
||||
scenario::publish_target(&format!("selection:action:{index}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("selection:action:{index}"), response.rect);
|
||||
if response.clicked() {
|
||||
clicked = Some(index);
|
||||
}
|
||||
@@ -827,8 +827,8 @@ fn icon_label_presentation(ui: &mut Ui, label: &str, icon: Option<&str>, enabled
|
||||
// A scope cannot reserve its natural width in the parent's wrapping row.
|
||||
let response = if super::settings_page_active(ui) { ui.add_enabled(enabled, paint) } else { ui.add_enabled_ui(enabled, paint).inner };
|
||||
let key = format!("dialog:button:{}:{label}", super::layout::label(ui));
|
||||
scenario::publish_handle(&key, response.rect.center());
|
||||
scenario::publish_rect(&key, response.rect);
|
||||
solidcraft_ui_registry::publish_handle(&key, response.rect.center());
|
||||
solidcraft_ui_registry::publish_rect(&key, response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ pub(crate) fn assistant_approval<R>(ui: &mut Ui, title: &str, body: impl FnOnce(
|
||||
.inner
|
||||
},
|
||||
);
|
||||
crate::scenario::publish_rect("assistant:approval", output.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("assistant:approval", output.response.rect);
|
||||
output
|
||||
}
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ pub(crate) fn checkbox(ui: &mut Ui, label: &str, value: &mut bool) -> Response {
|
||||
/// Mixed values become checked when activated; the caller owns whether selection is mixed.
|
||||
/// Tokens: space_14/3/2/6, radius_sm, control_fill/border, form_primary/ink, hover, on_accent.
|
||||
pub(crate) fn checkbox_indeterminate(ui: &mut Ui, label: &str, value: &mut bool, mixed: bool) -> Response {
|
||||
crate::prefs_search::note_unlabelled(ui, &[label]);
|
||||
if crate::prefs_search::querying(ui) && !crate::prefs_search::visible(ui, &[label]) {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
super::settings_search::note_unlabelled(ui, &[label]);
|
||||
if super::settings_search::querying(ui) && !super::settings_search::visible(ui, &[label]) {
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
let t = Tokens::get();
|
||||
let font = egui::FontId::proportional(t.font_label);
|
||||
@@ -75,8 +75,8 @@ pub(crate) fn checkbox_indeterminate(ui: &mut Ui, label: &str, value: &mut bool,
|
||||
let width = (t.space_14 + gap + galley.size().x).max(ui.spacing().interact_size.y).min(super::metrics::dimension(ui.available_width()));
|
||||
let height = ui.spacing().interact_size.y.max(t.space_14).max(galley.size().y);
|
||||
let (rect, mut response) = ui.allocate_exact_size(vec2(width, height), egui::Sense::click());
|
||||
crate::prefs_search::remember(ui, label, rect);
|
||||
crate::prefs_search::follow(ui, label, rect);
|
||||
super::settings_search::remember(ui, label, rect);
|
||||
super::settings_search::follow(ui, label, rect);
|
||||
if response.clicked() {
|
||||
*value = mixed || !*value;
|
||||
response.mark_changed();
|
||||
|
||||
@@ -5,9 +5,9 @@ use super::{
|
||||
metrics::*,
|
||||
text::paint_text,
|
||||
};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use egui::{Ui, vec2};
|
||||
|
||||
/// Text segments retain mixed states and optional compact clipping instead of dropdown fallback.
|
||||
@@ -36,12 +36,12 @@ pub(crate) struct Segments<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], config: Segments<'_>) -> bool {
|
||||
crate::prefs_search::note_unlabelled(ui, options);
|
||||
if crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, options) {
|
||||
super::settings_search::note_unlabelled(ui, options);
|
||||
if super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, options) {
|
||||
return false;
|
||||
}
|
||||
if crate::prefs_search::querying(ui) && !crate::prefs_search::shows_all(ui) {
|
||||
crate::prefs_search::reveal(ui);
|
||||
if super::settings_search::querying(ui) && !super::settings_search::shows_all(ui) {
|
||||
super::settings_search::reveal(ui);
|
||||
}
|
||||
let Segments {
|
||||
focus,
|
||||
@@ -194,11 +194,11 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
);
|
||||
t.button_bevel_with_radius(ui, &response, on, rounding);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, segment_enabled, on, *text));
|
||||
scenario::publish_target(&format!("dialog:{}:{option}", label(ui)), segment);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:{}:{option}", label(ui)), segment);
|
||||
if let Some(target) = targets.get(index) {
|
||||
let visible = segment.intersect(ui.clip_rect());
|
||||
if visible.is_positive() {
|
||||
scenario::publish_target(target, visible);
|
||||
solidcraft_ui_registry::publish_target(target, visible);
|
||||
}
|
||||
}
|
||||
let response =
|
||||
@@ -216,7 +216,7 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
scenario::publish_target_as(&format!("dialog:{}", label(ui)), &format!("dialog:segments:{}", label(ui)), rect);
|
||||
solidcraft_ui_registry::publish_target_as(&format!("dialog:{}", label(ui)), &format!("dialog:segments:{}", label(ui)), rect);
|
||||
clicked
|
||||
}
|
||||
|
||||
@@ -280,8 +280,8 @@ pub(crate) fn segmented_icons(ui: &mut Ui, selected: &mut usize, options: &[Icon
|
||||
}
|
||||
t.button_bevel_with_radius(ui, &response, on, rounding);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), on, option.key));
|
||||
scenario::publish_handle(&format!("dialog:{}:{}", label(ui), option.key), segment.center());
|
||||
scenario::publish_target(option.key, segment);
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{}:{}", label(ui), option.key), segment.center());
|
||||
solidcraft_ui_registry::publish_target(option.key, segment);
|
||||
let response = response.backing_on_hover_text(&option.tip);
|
||||
if response.clicked() {
|
||||
*selected = index;
|
||||
@@ -291,7 +291,7 @@ pub(crate) fn segmented_icons(ui: &mut Ui, selected: &mut usize, options: &[Icon
|
||||
if !catalog && t.borders.bevel.is_none() {
|
||||
ui.painter().rect_stroke(rect, t.control_radius, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
|
||||
}
|
||||
scenario::publish_target_as(&format!("dialog:{}", label(ui)), &format!("dialog:segments:{}", label(ui)), rect);
|
||||
solidcraft_ui_registry::publish_target_as(&format!("dialog:{}", label(ui)), &format!("dialog:segments:{}", label(ui)), rect);
|
||||
clicked
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ pub(crate) fn checkbox_grid(ui: &mut Ui, items: &mut [(&str, &mut bool)], column
|
||||
let text_rect = egui::Rect::from_min_max(egui::pos2((square.right() + t.space_6).min(rect.right()), rect.top()), rect.max);
|
||||
paint_text(ui, text_rect, text, egui::FontId::proportional(t.font_body), t.form_ink, false);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::CheckBox, ui.is_enabled(), **value, translated));
|
||||
scenario::publish_target_as(&format!("dialog:{label}"), &format!("dialog:checkbox:{label}"), rect);
|
||||
solidcraft_ui_registry::publish_target_as(&format!("dialog:{label}"), &format!("dialog:checkbox:{label}"), rect);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -344,7 +344,7 @@ pub(crate) fn checkbox_row(ui: &mut egui::Ui, key: &str, label: &str, on: bool)
|
||||
.with_clip_rect(ui.clip_rect().intersect(egui::Rect::from_min_max(rect.min, egui::pos2(square.left() - t.space_6, rect.bottom()))));
|
||||
painter.text(rect.left_center(), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(t.font_label), t.form_ink);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::CheckBox, ui.is_enabled(), on, label));
|
||||
crate::scenario::publish_target(&format!("sketch_palette:toggle:{key}"), rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("sketch_palette:toggle:{key}"), rect);
|
||||
response.backing_on_hover_text(label).clicked()
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ pub(crate) fn text_emphasis(ui: &mut Ui, bold: &mut bool, italic: &mut bool, bol
|
||||
response = response.backing_on_disabled_hover_text(crate::i18n::tr("sketch_text.no_bold"));
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled() && enabled, *value, crate::i18n::tr(id)));
|
||||
scenario::publish_target_as(&format!("dialog:{key}"), &format!("dialog:emphasis:{key}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target_as(&format!("dialog:{key}"), &format!("dialog:emphasis:{key}"), response.rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ pub(crate) fn tool_choices<'a>(ui: &mut Ui, label: &str, current: &str, choices:
|
||||
);
|
||||
crate::icons::paint_icon(ui.painter(), response.rect.shrink(t.space_6), choice.icon, f32::from(t.icon.a()) / 255.0);
|
||||
t.button_bevel(ui, &response, on);
|
||||
scenario::publish_target(&format!("sketch_palette:feature:{}", choice.key), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("sketch_palette:feature:{}", choice.key), response.rect);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, enabled, on, choice.label));
|
||||
if response.backing_on_hover_text(choice.label).clicked() && enabled {
|
||||
start = Some(choice.key);
|
||||
@@ -418,11 +418,11 @@ pub(crate) fn toggle_icon(ui: &mut Ui, row: &str, label: &str, selected: &mut bo
|
||||
crate::icons::paint_icon(ui.painter(), response.rect.shrink(t.space_3), icon, f32::from(t.icon.a()) / 255.0);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, *selected, crate::i18n::tr_label(label)));
|
||||
t.button_bevel(ui, &response, *selected);
|
||||
crate::scenario::publish_handle(&format!("dialog:{row}:{label}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{row}:{label}"), response.rect.center());
|
||||
if response.clone().backing_on_hover_text(crate::i18n::tr_label(label)).clicked() {
|
||||
*selected = !*selected;
|
||||
}
|
||||
scenario::publish_rect(&format!("dialog:{row}:{label}"), response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:{row}:{label}"), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -435,15 +435,15 @@ mod tests;
|
||||
pub(crate) fn list_picker(ui: &mut Ui, key: &str, text: &str, selected: bool) -> egui::Response {
|
||||
let response =
|
||||
ui.add_sized(egui::vec2(super::metrics::dimension(ui.available_width()), super::row_height()), egui::Button::selectable(selected, text));
|
||||
crate::scenario::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
/// Intrinsic-width selection chip for horizontal lists, sharing list-picker styling.
|
||||
pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) -> egui::Response {
|
||||
crate::prefs_search::note_unlabelled(ui, &[text]);
|
||||
if crate::prefs_search::querying(ui) && !crate::prefs_search::visible(ui, &[text]) {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
super::settings_search::note_unlabelled(ui, &[text]);
|
||||
if super::settings_search::querying(ui) && !super::settings_search::visible(ui, &[text]) {
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
let t = Tokens::get();
|
||||
if super::settings_page_active(ui) {
|
||||
@@ -456,9 +456,9 @@ pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) ->
|
||||
.stroke(egui::Stroke::new(1.0, if selected { t.form_primary } else { t.control_border }))
|
||||
.corner_radius(t.radius_xl),
|
||||
);
|
||||
crate::prefs_search::remember(ui, text, response.rect);
|
||||
crate::prefs_search::follow(ui, text, response.rect);
|
||||
crate::scenario::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
super::settings_search::remember(ui, text, response.rect);
|
||||
super::settings_search::follow(ui, text, response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
return response;
|
||||
}
|
||||
if super::layout::in_catalog(ui) {
|
||||
@@ -475,13 +475,13 @@ pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) ->
|
||||
false,
|
||||
);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), selected, text));
|
||||
crate::scenario::publish_target(&format!("dialog:list:{key}"), rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:list:{key}"), rect);
|
||||
return response;
|
||||
}
|
||||
let width = (ui.painter().layout_no_wrap(text.to_owned(), crate::fonts::semibold(t.font_label), t.text).size().x + t.space_24)
|
||||
.clamp(t.control_height * 2.0, t.form_card_width);
|
||||
let response = ui.add_sized(vec2(width, super::row_height()), egui::Button::selectable(selected, text).truncate());
|
||||
crate::scenario::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ pub(crate) fn thumbnail_choice(ui: &mut Ui, key: &str, text: &str, state: Thumbn
|
||||
if state.modified {
|
||||
let dot = egui::pos2(rect.right() - t.space_6, rect.top() + t.space_6);
|
||||
ui.painter().circle_filled(dot, t.space_3, t.warning);
|
||||
crate::scenario::publish_rect(&format!("dialog:list:{key}:modified"), egui::Rect::from_center_size(dot, vec2(t.space_6, t.space_6)));
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:list:{key}:modified"), egui::Rect::from_center_size(dot, vec2(t.space_6, t.space_6)));
|
||||
}
|
||||
if let Some(progress) = state.progress {
|
||||
let center = thumb.center();
|
||||
@@ -573,10 +573,10 @@ pub(crate) fn thumbnail_choice(ui: &mut Ui, key: &str, text: &str, state: Thumbn
|
||||
})
|
||||
.collect();
|
||||
ui.painter().add(egui::Shape::line(points, egui::Stroke::new(t.space_2, accent.ink)));
|
||||
crate::scenario::publish_rect(&format!("dialog:list:{key}:progress"), thumb);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:list:{key}:progress"), thumb);
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), state.selected, text));
|
||||
crate::scenario::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
response.backing_on_hover_text(text)
|
||||
}
|
||||
|
||||
@@ -745,7 +745,7 @@ pub(crate) fn popup_list(
|
||||
// Tip the full name first: long identifiers still clip inside a capped popup.
|
||||
let tip = if option.detail.is_empty() { option.text.to_owned() } else { format!("{}\n{}", option.text, option.detail) };
|
||||
let response = response.backing_on_hover_text(tip);
|
||||
crate::scenario::publish_handle(&format!("dialog:suggestion:{index}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:suggestion:{index}"), response.rect.center());
|
||||
if response.clicked() {
|
||||
chosen = Some(index);
|
||||
}
|
||||
@@ -759,7 +759,7 @@ pub(crate) fn popup_list(
|
||||
})
|
||||
});
|
||||
});
|
||||
crate::scenario::publish_rect("dialog:suggestions", area.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:suggestions", area.response.rect);
|
||||
ctx.data_mut(|d| d.insert_temp(id.with("rows"), rows));
|
||||
chosen
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Document RGB and optional separate opacity editing. Interface paint uses tokens; gradients are colour data.
|
||||
use super::recent_colors::{MAX_RECENT_COLORS, recent_colors, remember_color};
|
||||
use super::{layout::label, metrics::dimension};
|
||||
use crate::recent_colors::{MAX_RECENT_COLORS, recent_colors, remember_color};
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::{i18n, scenario, theme::Tokens};
|
||||
use crate::{i18n, theme::Tokens};
|
||||
use egui::{Color32, Response, Sense, Ui, ecolor::HsvaGamma, pos2, vec2};
|
||||
|
||||
fn publish(key: &str, response: &Response) {
|
||||
scenario::publish_rect(key, response.rect);
|
||||
scenario::publish_handle(key, response.rect.center());
|
||||
solidcraft_ui_registry::publish_rect(key, response.rect);
|
||||
solidcraft_ui_registry::publish_handle(key, response.rect.center());
|
||||
}
|
||||
|
||||
/// Linear RGB swatch and precise channel fields. The preview is clipped to display gamut;
|
||||
@@ -144,7 +144,7 @@ fn color_popover_with_opacity(ui: &mut Ui, rgb: &mut [u8; 3], mut opacity: Optio
|
||||
let rect =
|
||||
egui::Rect::from_min_size(rect.min + vec2(index as f32 * rect.width() / 3.0, 0.0), vec2(rect.width() / 3.0, rect.height()));
|
||||
ui.interact(rect, ui.scope_id().with(("format_tip", index)), Sense::hover()).backing_on_hover_text(i18n::tr(key));
|
||||
scenario::publish_handle(&format!("color:format:{name}"), rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("color:format:{name}"), rect.center());
|
||||
}
|
||||
});
|
||||
hexadecimal = selected == 2;
|
||||
@@ -296,7 +296,7 @@ fn color_popover_with_opacity(ui: &mut Ui, rgb: &mut [u8; 3], mut opacity: Optio
|
||||
}
|
||||
});
|
||||
// A colour joins the recent list once an edit is finished: a drag or a held step button is
|
||||
// released, a typed value is entered. Every picker shares the list (`crate::recent_colors`).
|
||||
// released, a typed value is entered. Every picker shares the list (`super::recent_colors`).
|
||||
// While the popover stays open, fine-tuning replaces the colour it added rather than filling
|
||||
// the list with each step; a colour taken from the list starts a new entry.
|
||||
let session_id = id.with("recent_session");
|
||||
@@ -320,7 +320,7 @@ fn color_popover_with_opacity(ui: &mut Ui, rgb: &mut [u8; 3], mut opacity: Optio
|
||||
d.insert_temp(hex_id, hex);
|
||||
d.insert_temp(format_id, hexadecimal);
|
||||
});
|
||||
scenario::publish_rect("color:popover", ui.min_rect());
|
||||
solidcraft_ui_registry::publish_rect("color:popover", ui.min_rect());
|
||||
*rgb != before_rgb || opacity.as_deref().copied() != before_opacity || hsv != before || accepted_mixed
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ fn spin_channel(ui: &mut Ui, name: &str, tip: Option<&str>, value: &mut f64, spi
|
||||
egui::DragValue::new(value)
|
||||
.range(0.0..=max)
|
||||
.speed(speed)
|
||||
.custom_parser(crate::prefs::parse_number)
|
||||
.custom_parser(super::parse_number)
|
||||
.custom_formatter(move |v, _| format!("{v:.decimals$}")),
|
||||
);
|
||||
response.widget_info(|| {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::scenario;
|
||||
use crate::ui_kit::test_support::pointer;
|
||||
|
||||
fn frame<R>(ctx: &egui::Context, events: Vec<egui::Event>, mut add: impl FnMut(&mut Ui) -> R) {
|
||||
@@ -473,11 +474,11 @@ fn recent_list_is_capped_unique_and_synced_with_the_saved_one() {
|
||||
assert_eq!(recent.iter().filter(|c| **c == [35, 0, 0]).count(), 1);
|
||||
// Picks flow to the saved list once…
|
||||
let mut saved = Vec::new();
|
||||
crate::recent_colors::share(&ctx, &mut saved);
|
||||
crate::ui_kit::recent_colors::share(&ctx, &mut saved);
|
||||
assert_eq!(saved, recent);
|
||||
// …and a list loaded from preferences (even an oversized one) is what the pickers show.
|
||||
let mut loaded: Vec<[u8; 3]> = (0..100u8).map(|i| [0, i, 0]).collect();
|
||||
crate::recent_colors::share(&ctx, &mut loaded);
|
||||
crate::ui_kit::recent_colors::share(&ctx, &mut loaded);
|
||||
assert_eq!(loaded.len(), MAX_RECENT_COLORS);
|
||||
assert_eq!(recent_colors(&ctx), loaded);
|
||||
}
|
||||
@@ -492,9 +493,9 @@ fn recent_colours_are_saved_with_the_preferences_and_hostile_entries_dropped() {
|
||||
assert_eq!(app.ui.recent_colors.len(), MAX_RECENT_COLORS);
|
||||
assert_eq!(app.ui.recent_colors.first(), Some(&[1, 2, 3]));
|
||||
let ctx = crate::fonts::test_context();
|
||||
crate::recent_colors::share(&ctx, &mut app.ui.recent_colors);
|
||||
crate::ui_kit::recent_colors::share(&ctx, &mut app.ui.recent_colors);
|
||||
remember_color(&ctx, [9, 8, 7], None);
|
||||
crate::recent_colors::share(&ctx, &mut app.ui.recent_colors);
|
||||
crate::ui_kit::recent_colors::share(&ctx, &mut app.ui.recent_colors);
|
||||
let mut restored = crate::SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
restored.load_prefs(&app.prefs());
|
||||
assert_eq!(restored.ui.recent_colors.first(), Some(&[9, 8, 7]));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Shared image-comparison divider and corner chips. Geometry and ink use kit tokens.
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Rect, Sense, Ui, pos2, vec2};
|
||||
|
||||
pub(crate) fn comparison_position(position: f32) -> f32 {
|
||||
@@ -21,7 +21,7 @@ pub(crate) fn comparison_divider(ui: &mut Ui, canvas: Rect, position: &mut f32,
|
||||
}
|
||||
comparison_divider_chrome(ui, canvas, *position);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Slider, ui.is_enabled(), label));
|
||||
scenario::publish_target("render:compare:divider", rail);
|
||||
solidcraft_ui_registry::publish_target("render:compare:divider", rail);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(crate) fn comparison_divider_chrome(ui: &Ui, canvas: Rect, position: f32) {
|
||||
for offset in [-t.space_6, 0.0, t.space_6] {
|
||||
painter.circle_filled(handle.center() + vec2(0.0, offset), t.space_2, accent.ink);
|
||||
}
|
||||
scenario::publish_rect("render:compare:handle", handle);
|
||||
solidcraft_ui_registry::publish_rect("render:compare:handle", handle);
|
||||
}
|
||||
|
||||
/// Token-painted chip anchored inside either clipped half of the image canvas.
|
||||
@@ -51,5 +51,5 @@ pub(crate) fn comparison_badge(ui: &mut Ui, bounds: Rect, label: &str, right: bo
|
||||
painter.rect_filled(rect, t.control_radius, t.panel);
|
||||
painter.rect_stroke(rect, t.control_radius, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
|
||||
painter.with_clip_rect(rect).galley(pos2(rect.left() + t.space_8, rect.center().y - galley.size().y * 0.5), galley, t.form_ink);
|
||||
scenario::publish_rect(if right { "render:compare:right-label" } else { "render:compare:left-label" }, rect);
|
||||
solidcraft_ui_registry::publish_rect(if right { "render:compare:right-label" } else { "render:compare:left-label" }, rect);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Modal confirmations share the Panel/Window frame and the single kit footer.
|
||||
use super::{Action, Footer, Window, actions::footer_with_left, text::paint_text};
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Ui, vec2};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -68,7 +68,7 @@ impl<'a> Confirm<'a> {
|
||||
pub(crate) fn show(self, ctx: &egui::Context, valid: bool, message: impl FnOnce(&mut Ui)) -> (bool, Action) {
|
||||
// Reviewed reports and native pickers temporarily own input. Keep the pending
|
||||
// answer untouched and resume the modal when that boundary closes.
|
||||
if crate::issue_report::cad_input_blocked() {
|
||||
if super::input_blocked() {
|
||||
return (false, Action::None);
|
||||
}
|
||||
self.window.surface(ctx, true, |ui| self.content(ui, valid, message))
|
||||
@@ -104,7 +104,7 @@ impl<'a> Confirm<'a> {
|
||||
} else {
|
||||
crate::icons::paint_accented(ui.painter(), disc.shrink(t.space_8), self.icon, accent, 1.0);
|
||||
}
|
||||
scenario::publish_rect("confirm:disc", disc);
|
||||
solidcraft_ui_registry::publish_rect("confirm:disc", disc);
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(ui.available_width().max(0.0));
|
||||
let (heading, _) = ui.allocate_exact_size(vec2(ui.available_width().max(0.0), t.form_close_size), egui::Sense::hover());
|
||||
@@ -118,8 +118,8 @@ impl<'a> Confirm<'a> {
|
||||
.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), self.title));
|
||||
let response = super::icon_button_at(ui, close, "close", "Close");
|
||||
closed = response.clicked();
|
||||
scenario::publish_target("dialog:close", response.rect);
|
||||
scenario::publish_rect("dialog:header", heading);
|
||||
solidcraft_ui_registry::publish_target("dialog:close", response.rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:header", heading);
|
||||
message(ui);
|
||||
if let Some(detail) = self.detail {
|
||||
super::list_frame(ui, "confirm_detail", |ui| {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
//! A surface declares its purpose; the shell supplies the layout once per frame. Never infer
|
||||
//! floating/docked presentation from a panel name or keep a second layout preference here.
|
||||
use crate::frame::Side;
|
||||
use crate::{shell::LayoutMode, theme::Tokens};
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Color32, Context, Frame, Id, Stroke, WidgetText};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) type Background = fn(&egui::Painter, Frame, egui::Rect, egui::Shape) -> egui::Shape;
|
||||
|
||||
@@ -464,9 +465,45 @@ impl DockEdges {
|
||||
pub const ALL: Self = Self { left: true, right: true, top: true, bottom: true };
|
||||
}
|
||||
|
||||
/// Saved workspace layout; automation may override it for a session.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LayoutMode {
|
||||
#[default]
|
||||
Floating,
|
||||
#[serde(rename = "compact", alias = "docked")]
|
||||
Docked,
|
||||
}
|
||||
|
||||
impl LayoutMode {
|
||||
pub(crate) fn publish(self, ctx: &egui::Context) {
|
||||
ctx.data_mut(|data| data.insert_temp(egui::Id::unique("sc_container_layout"), self));
|
||||
}
|
||||
|
||||
pub(crate) fn current(ctx: &egui::Context) -> Self {
|
||||
ctx.data(|data| data.get_temp(egui::Id::unique("sc_container_layout"))).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The gap from the viewport edge to the floating cards and between the cards.
|
||||
pub(crate) const SHELL_GAP: f32 = 7.0;
|
||||
|
||||
thread_local! {
|
||||
/// The areas built as [`Surface::Canvas`]: a value box, a dimension's editor, a readout.
|
||||
static CANVAS_AREAS: std::cell::RefCell<std::collections::HashSet<Id>> = std::cell::RefCell::default();
|
||||
/// Set by the host while something else owns the input over the CAD view (a reviewed issue
|
||||
/// report, a native file picker): kit surfaces other than [`Surface::InputShield`] take none.
|
||||
static INPUT_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Does the host hold the CAD input back this frame? See [`set_input_blocked`].
|
||||
pub(crate) fn input_blocked() -> bool {
|
||||
INPUT_BLOCKED.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// The host marks the CAD input as blocked, or no longer blocked.
|
||||
pub(crate) fn set_input_blocked(blocked: bool) {
|
||||
INPUT_BLOCKED.with(|cell| cell.set(blocked));
|
||||
}
|
||||
|
||||
/// Is `layer` a model-anchored editor or readout ([`Surface::Canvas`])? Such an area sits on the
|
||||
@@ -583,7 +620,7 @@ impl Container {
|
||||
tip.show(|ui| {
|
||||
let body = |ui: &mut egui::Ui| {
|
||||
ui.label(text);
|
||||
crate::scenario::publish_rect("kit:tooltip", ui.min_rect());
|
||||
solidcraft_ui_registry::publish_rect("kit:tooltip", ui.min_rect());
|
||||
};
|
||||
match slot {
|
||||
Some(slot) => slot.body(ui, body),
|
||||
@@ -607,7 +644,7 @@ impl Container {
|
||||
tip.show(|ui| {
|
||||
let body = |ui: &mut egui::Ui| {
|
||||
add(ui);
|
||||
crate::scenario::publish_rect("kit:tooltip", ui.min_rect());
|
||||
solidcraft_ui_registry::publish_rect("kit:tooltip", ui.min_rect());
|
||||
};
|
||||
match slot {
|
||||
Some(slot) => slot.body(ui, body),
|
||||
@@ -629,7 +666,7 @@ impl Container {
|
||||
|
||||
/// Separation between adaptive workspace surfaces; compact surfaces meet their edge.
|
||||
pub fn gap(self) -> f32 {
|
||||
if self.floating() { crate::shell::SHELL_GAP } else { 0.0 }
|
||||
if self.floating() { SHELL_GAP } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Tokenised outer chrome. Callers may supply content padding, not another card surface.
|
||||
@@ -1085,6 +1122,6 @@ impl Container {
|
||||
}
|
||||
|
||||
fn input_enabled(self) -> bool {
|
||||
self.surface == Surface::InputShield || !crate::issue_report::cad_input_blocked()
|
||||
self.surface == Surface::InputShield || !input_blocked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Description-driven details content. No tool, document or command knowledge lives here.
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{fonts::RichTextExt as _, scenario, theme::Tokens};
|
||||
use crate::{fonts::RichTextExt as _, theme::Tokens};
|
||||
use egui::{RichText, Ui};
|
||||
|
||||
const FIELD_FOCUS: &str = "details_field_focus";
|
||||
@@ -201,7 +201,7 @@ fn setting(ui: &mut Ui, field: &DetailSetting, focus: bool) -> Option<DetailEven
|
||||
}
|
||||
};
|
||||
if let Some(response) = response {
|
||||
scenario::publish_target(&format!("details:field:{}", field.id), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("details:field:{}", field.id), response.rect);
|
||||
// Visibility reads Context::will_discard. Never re-enter Context while
|
||||
// holding its data write lock.
|
||||
let visible = ui.is_visible() && ui.is_enabled() && ui.is_rect_visible(response.rect);
|
||||
@@ -235,7 +235,7 @@ fn setting(ui: &mut Ui, field: &DetailSetting, focus: bool) -> Option<DetailEven
|
||||
&& ![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]].contains(vector)
|
||||
{
|
||||
let response = ui.add(egui::Label::new(RichText::new(current).monospace()).wrap()).backing_on_hover_text(current);
|
||||
scenario::publish_target(&format!("details:current:{}", field.id), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("details:current:{}", field.id), response.rect);
|
||||
}
|
||||
changed.then(|| DetailEvent::Change(field.id.clone(), value))
|
||||
}
|
||||
@@ -257,7 +257,7 @@ pub(crate) fn details(ui: &mut Ui, id: &str, description: &Details, max_height:
|
||||
if let Some(caption) = &description.caption {
|
||||
let response = ui.add(egui::Label::new(RichText::new(caption).size(t.font_label).color(t.text_dim)).wrap());
|
||||
caption_height = response.rect.height() + t.space_8;
|
||||
scenario::publish_rect("details:caption", response.rect);
|
||||
solidcraft_ui_registry::publish_rect("details:caption", response.rect);
|
||||
}
|
||||
let footer_height = if description.actions.is_empty() { 0.0 } else { t.control_height + t.space_16 };
|
||||
let body = egui::ScrollArea::vertical()
|
||||
@@ -318,7 +318,7 @@ pub(crate) fn details(ui: &mut Ui, id: &str, description: &Details, max_height:
|
||||
.push_id(&row.id, |ui| super::icon_button_at(ui, rect, "fit", crate::i18n::tr("ui.details_target")))
|
||||
.inner;
|
||||
remember_control(ui, response.id);
|
||||
scenario::publish_target(&format!("details:target:{}", row.id), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("details:target:{}", row.id), response.rect);
|
||||
if response.clicked() {
|
||||
events.push(DetailEvent::Target(row.id.clone()));
|
||||
}
|
||||
@@ -327,7 +327,7 @@ pub(crate) fn details(ui: &mut Ui, id: &str, description: &Details, max_height:
|
||||
});
|
||||
}
|
||||
});
|
||||
scenario::publish_count(
|
||||
solidcraft_ui_registry::publish_count(
|
||||
"details:findings_visible",
|
||||
if expanded { description.findings.len() } else { description.findings.len().min(6) } as f64,
|
||||
);
|
||||
@@ -335,7 +335,7 @@ pub(crate) fn details(ui: &mut Ui, id: &str, description: &Details, max_height:
|
||||
let text = crate::i18n::format("ui.details_show_all", &[("count", &description.findings.len().to_string())]);
|
||||
let response = super::button(ui, text, super::Button::Ghost);
|
||||
remember_control(ui, response.id);
|
||||
scenario::publish_target("details:show_all", response.rect);
|
||||
solidcraft_ui_registry::publish_target("details:show_all", response.rect);
|
||||
if response.clicked() {
|
||||
ui.data_mut(|data| data.insert_temp(expanded_id, true));
|
||||
}
|
||||
@@ -357,7 +357,7 @@ pub(crate) fn details(ui: &mut Ui, id: &str, description: &Details, max_height:
|
||||
});
|
||||
}
|
||||
});
|
||||
scenario::publish_count(
|
||||
solidcraft_ui_registry::publish_count(
|
||||
"details:readout_visible",
|
||||
if expanded { description.readout.len() } else { description.readout.len().min(6) } as f64,
|
||||
);
|
||||
@@ -365,27 +365,27 @@ pub(crate) fn details(ui: &mut Ui, id: &str, description: &Details, max_height:
|
||||
let text = crate::i18n::format("ui.details_show_all", &[("count", &description.readout.len().to_string())]);
|
||||
let response = super::button(ui, text, super::Button::Ghost);
|
||||
remember_control(ui, response.id);
|
||||
scenario::publish_target("details:show_all", response.rect);
|
||||
solidcraft_ui_registry::publish_target("details:show_all", response.rect);
|
||||
if response.clicked() {
|
||||
ui.data_mut(|data| data.insert_temp(expanded_id, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
scenario::publish_rect("details:body", body.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect("details:body", body.inner_rect);
|
||||
if !description.actions.is_empty() {
|
||||
super::divider(ui, "details_actions");
|
||||
let actions = ui.horizontal(|ui| {
|
||||
for (id, label) in description.actions.iter().take(2) {
|
||||
let response = super::text_button(ui, label, true);
|
||||
remember_control(ui, response.id);
|
||||
scenario::publish_target(&format!("details:action:{id}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("details:action:{id}"), response.rect);
|
||||
if response.clicked() {
|
||||
events.push(DetailEvent::Action(id.clone()));
|
||||
}
|
||||
}
|
||||
});
|
||||
scenario::publish_rect("details:actions", actions.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("details:actions", actions.response.rect);
|
||||
}
|
||||
});
|
||||
events
|
||||
|
||||
@@ -4,10 +4,11 @@ use super::{
|
||||
metrics::*,
|
||||
text::paint_text,
|
||||
};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use egui::{Response, RichText, Ui, vec2};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// One alignment policy for every kit text input. Align the galley itself rather than
|
||||
/// approximating font metrics with integer margins (Plex Mono and Sans differ).
|
||||
@@ -16,6 +17,48 @@ pub(super) fn text_input(edit: egui::TextEdit<'_>) -> egui::TextEdit<'_> {
|
||||
edit.align(egui::Align2::LEFT_CENTER).margin(vec2(t.form_inset, 0.0)).min_size(vec2(0.0, t.control_height))
|
||||
}
|
||||
|
||||
/// The host's expression language, as the kit's value fields use it. The kit draws and edits the
|
||||
/// text; it never parses expressions itself. The host installs one per context with
|
||||
/// [`install_expressions`]. Without one, a field's label drag changes nothing (no text is a
|
||||
/// literal) and an editor offers no completion or stepping.
|
||||
pub trait Expressions: Send + Sync {
|
||||
/// Whether the trimmed `text` is a plain literal: a number with an optional unit ("20 mm",
|
||||
/// "-1.5 in"), not a parameter or a formula. Only a literal can be changed by a label drag.
|
||||
fn is_literal(&self, text: &str) -> bool;
|
||||
/// Whether `unit`, the text after a literal's number as written (" mm", "in", ""), is a unit
|
||||
/// of the language. An empty unit is a unit-less number.
|
||||
fn is_unit(&self, unit: &str) -> bool;
|
||||
/// Offer completion and stepping for this frame under the editor `response` over `text`.
|
||||
/// Returns true when either changed the text; that frame's Enter then accepts the change
|
||||
/// instead of submitting.
|
||||
fn complete(&self, ui: &mut Ui, response: &Response, text: &mut String) -> bool;
|
||||
}
|
||||
|
||||
/// Where a context keeps its installed [`Expressions`].
|
||||
fn expressions_id() -> egui::Id {
|
||||
egui::Id::unique("sc_kit_expressions")
|
||||
}
|
||||
|
||||
/// Give every kit value field in `ctx` the host's expression language. Installing again replaces it.
|
||||
pub fn install_expressions(ctx: &egui::Context, expressions: Arc<dyn Expressions>) {
|
||||
ctx.data_mut(|data| data.insert_temp(expressions_id(), expressions));
|
||||
}
|
||||
|
||||
/// The expression language installed in `ctx`, if any.
|
||||
pub(crate) fn expressions(ctx: &egui::Context) -> Option<Arc<dyn Expressions>> {
|
||||
ctx.data(|data| data.get_temp::<Arc<dyn Expressions>>(expressions_id()))
|
||||
}
|
||||
|
||||
/// A plain number typed into a field, with a decimal point or a decimal comma (#1347): "2.5"
|
||||
/// and "2,5" are both 2.5, as a numeric keypad types either depending on the locale. Spaces are
|
||||
/// ignored and "−" is a minus, as in egui's own parser. A comma beside a point ("1.234,5") or a
|
||||
/// second comma is not guessed at: the text is not a number.
|
||||
pub(crate) fn parse_number(text: &str) -> Option<f64> {
|
||||
let t: String = text.chars().filter(|c| !c.is_whitespace()).map(|c| if c == '\u{2212}' { '-' } else { c }).collect();
|
||||
let t = if !t.contains('.') && t.matches(',').count() == 1 { t.replacen(',', ".", 1) } else { t };
|
||||
t.parse::<f64>().ok().filter(|v| v.is_finite())
|
||||
}
|
||||
|
||||
/// Expression editor with the application's existing completion and number-focus behaviour.
|
||||
/// Values carry their own unit text ("20 mm", "45 deg"): there is no unit suffix. Returns accepted Enter.
|
||||
/// Tokens: `control_height, control_fill, form_inset, font_label, space_4; scope tokens`.
|
||||
@@ -86,7 +129,7 @@ pub(crate) fn count_field(ui: &mut Ui, value: &mut String, limits: &Count<'_>) -
|
||||
));
|
||||
let sign = if delta > 0 { "+1" } else { "−1" };
|
||||
arrow.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, format!("{} {sign}", crate::i18n::tr_label(&row))));
|
||||
scenario::publish_handle(&format!("dialog:{row} {name}"), half.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{row} {name}"), half.center());
|
||||
if let (true, true, Some(next)) = (enabled, arrow.clicked(), next) {
|
||||
*value = next;
|
||||
}
|
||||
@@ -140,18 +183,20 @@ struct Literal {
|
||||
/// The largest value a label drag reads or writes.
|
||||
const LITERAL_CAP: f64 = 1e12;
|
||||
|
||||
/// `text` as a [`Literal`], or None for anything else: a parameter, a formula, an empty field.
|
||||
/// Dragging never rewrites an expression, so a driving parameter stays driving.
|
||||
fn literal(text: &str) -> Option<Literal> {
|
||||
/// `text` as a [`Literal`] of the `expressions` language, or None for anything else: a parameter,
|
||||
/// a formula, an empty field. Dragging never rewrites an expression, so a driving parameter stays driving.
|
||||
fn literal(expressions: &dyn Expressions, text: &str) -> Option<Literal> {
|
||||
let text = text.trim();
|
||||
if !solidcraft_engine::doc::expr::is_literal(text) {
|
||||
if !expressions.is_literal(text) {
|
||||
return None;
|
||||
}
|
||||
let sign = usize::from(text.starts_with(['-', '+']));
|
||||
let end = text.char_indices().skip(sign).find(|(_, c)| !(c.is_ascii_digit() || *c == '.' || *c == ',')).map_or(text.len(), |(at, _)| at);
|
||||
let (number, unit) = (text.get(..end)?, text.get(end..)?);
|
||||
// An exponent ("1e3") or anything else that is not a unit is left alone.
|
||||
solidcraft_engine::doc::expr::unit_info(unit)?;
|
||||
if !expressions.is_unit(unit) {
|
||||
return None;
|
||||
}
|
||||
let comma = number.contains(',');
|
||||
let number: f64 = number.replace(',', ".").parse().ok().filter(|n: &f64| n.is_finite() && n.abs() < LITERAL_CAP)?;
|
||||
Some(Literal { number, unit: unit.to_owned(), comma })
|
||||
@@ -200,7 +245,7 @@ struct Scrub {
|
||||
/// holding an expression or a parameter is left alone. Returns whether the text changed.
|
||||
pub(super) fn scrub(ui: &Ui, label: &Response, value: &mut String) -> bool {
|
||||
let id = label.id.with("sc_scrub");
|
||||
let Some(now) = literal(value).filter(|_| ui.is_enabled()) else {
|
||||
let Some(now) = expressions(ui.ctx()).and_then(|e| literal(e.as_ref(), value)).filter(|_| ui.is_enabled()) else {
|
||||
ui.ctx().data_mut(|data| data.remove::<Scrub>(id));
|
||||
return false;
|
||||
};
|
||||
@@ -333,7 +378,7 @@ fn searchable_dropdown_with(
|
||||
menu: SearchMenu<'_>,
|
||||
) -> Option<String> {
|
||||
let SearchMenu { custom, pending, icon } = menu;
|
||||
crate::prefs_search::note_current(ui, current);
|
||||
super::settings_search::note_current(ui, current);
|
||||
let mut chosen = None;
|
||||
let row = label(ui).to_owned();
|
||||
let combo = egui::ComboBox::from_id_salt(id)
|
||||
@@ -355,12 +400,12 @@ fn searchable_dropdown_with(
|
||||
);
|
||||
// Preferences types into its own search only while no field has the keyboard.
|
||||
if search.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
scenario::publish_handle(&format!("dialog:{row} search"), search.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{row} search"), search.rect.center());
|
||||
if let Some(custom) = custom {
|
||||
let response = ui.selectable_label(false, custom);
|
||||
scenario::publish_handle(&format!("dialog:{row} file"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{row} file"), response.rect.center());
|
||||
if response.clicked() {
|
||||
chosen = Some(filter.trim().to_owned());
|
||||
ui.close();
|
||||
@@ -377,7 +422,7 @@ fn searchable_dropdown_with(
|
||||
for option in options.iter().filter(|o| o.label.to_lowercase().contains(&query)) {
|
||||
let response = ui.selectable_label(current == option.label, option.label);
|
||||
if let Some(handle) = option.handle {
|
||||
scenario::publish_handle(handle, response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(handle, response.rect.center());
|
||||
}
|
||||
if response.clicked() {
|
||||
chosen = Some(option.value.to_owned());
|
||||
@@ -453,12 +498,12 @@ pub(crate) enum Picked {
|
||||
/// list stays cheap. Returns the pick and the control's response.
|
||||
/// Tokens: dropdown tokens, field tokens, `space_24`, `font_small`, `text_dim`, `form_card_width`.
|
||||
pub(crate) fn search_picker(ui: &mut Ui, p: SearchPicker<'_>) -> egui::InnerResponse<Option<Picked>> {
|
||||
crate::prefs_search::note_current(ui, p.current);
|
||||
super::settings_search::note_current(ui, p.current);
|
||||
// As in `dropdown_menu_with_close_behavior`: only a search probe skips the real control.
|
||||
if crate::prefs_search::querying(ui)
|
||||
&& (!ui.is_visible() || (crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[p.current])))
|
||||
if super::settings_search::querying(ui)
|
||||
&& (!ui.is_visible() || (super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[p.current])))
|
||||
{
|
||||
return egui::InnerResponse::new(None, crate::prefs_search::hidden_response(ui));
|
||||
return egui::InnerResponse::new(None, super::settings_search::hidden_response(ui));
|
||||
}
|
||||
let state = egui::Id::unique(p.id);
|
||||
let mut picked = None;
|
||||
@@ -487,7 +532,7 @@ pub(crate) fn search_picker(ui: &mut Ui, p: SearchPicker<'_>) -> egui::InnerResp
|
||||
})
|
||||
.inner;
|
||||
publish(ui, &response, egui::Role::ComboBox);
|
||||
scenario::publish_target(p.handles, response.rect);
|
||||
solidcraft_ui_registry::publish_target(p.handles, response.rect);
|
||||
egui::InnerResponse::new(picked, response)
|
||||
}
|
||||
|
||||
@@ -513,7 +558,7 @@ fn search_picker_popup(ui: &mut Ui, p: &SearchPicker<'_>, state: egui::Id) -> Op
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
t.input_bevel(ui, &search);
|
||||
scenario::publish_target(&format!("{}:search", p.handles), search.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:search", p.handles), search.rect);
|
||||
// Asked until it has it: over a modal window egui drops the request of the opening's first frame.
|
||||
if !ui.data(|data| data.get_temp::<bool>(focused_id)).unwrap_or(false) {
|
||||
if search.has_focus() {
|
||||
@@ -524,7 +569,7 @@ fn search_picker_popup(ui: &mut Ui, p: &SearchPicker<'_>, state: egui::Id) -> Op
|
||||
}
|
||||
}
|
||||
if search.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
if search.changed() {
|
||||
ui.data_mut(|data| data.insert_temp(filter_id, filter.clone()));
|
||||
@@ -601,7 +646,7 @@ fn search_picker_popup(ui: &mut Ui, p: &SearchPicker<'_>, state: egui::Id) -> Op
|
||||
search_heading(ui, crate::i18n::tr("ui.agent_chat_custom_heading"), t.space_16);
|
||||
let text = crate::i18n::format("ui.agent_chat_model_custom", &[("model", &compact(model, SEARCH_LABEL_CAP))]);
|
||||
let row = search_row(ui, &text, false);
|
||||
scenario::publish_target(&format!("{}:custom", p.handles), row.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:custom", p.handles), row.rect);
|
||||
if row.clicked() {
|
||||
picked = Some(Picked::Custom(model.to_owned()));
|
||||
}
|
||||
@@ -621,14 +666,14 @@ fn search_picker_popup(ui: &mut Ui, p: &SearchPicker<'_>, state: egui::Id) -> Op
|
||||
SearchRow::Heading(group) => search_heading(ui, group, row_height),
|
||||
SearchRow::Group { name, count, open } => {
|
||||
let response = search_group_row(ui, name, *count, *open);
|
||||
scenario::publish_target(&format!("{}:group:{}", p.handles, name), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:group:{}", p.handles, name), response.rect);
|
||||
if response.clicked() {
|
||||
toggled = Some(((*name).to_owned(), !*open));
|
||||
}
|
||||
}
|
||||
SearchRow::Choice(choice) => {
|
||||
let response = search_row(ui, &compact(choice.label, SEARCH_LABEL_CAP), choice.value == p.current_value);
|
||||
scenario::publish_target(&format!("{}:{}", p.handles, choice.value), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:{}", p.handles, choice.value), response.rect);
|
||||
let response = if choice.tip.is_empty() {
|
||||
response
|
||||
} else {
|
||||
@@ -814,7 +859,7 @@ fn editor_reserving(ui: &mut Ui, value: &mut String, hint: &str, completion: boo
|
||||
}
|
||||
super::text_menu::menu(ui, &mut response, value, super::text_menu::Field::LINE);
|
||||
if response.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
if completion && ui.is_enabled() {
|
||||
ui.ctx().data_mut(|data| {
|
||||
@@ -834,7 +879,7 @@ fn editor_reserving(ui: &mut Ui, value: &mut String, hint: &str, completion: boo
|
||||
});
|
||||
super::layout::publish_geometry(ui, &response);
|
||||
// A completion's Enter accepts the suggestion, rather than submitting the dialog.
|
||||
let completed = completion && crate::params_dialog::complete(ui, &response, value);
|
||||
let completed = completion && expressions(ui.ctx()).is_some_and(|e| e.complete(ui, &response, value));
|
||||
(!completed && response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)), response.changed(), response)
|
||||
}
|
||||
|
||||
@@ -850,7 +895,7 @@ pub(crate) fn dropdown(ui: &mut Ui, id: &str, selected: &mut usize, options: &[&
|
||||
dropdown_menu(ui, id, text, |ui| {
|
||||
for (index, option) in options.iter().enumerate() {
|
||||
let response = ui.selectable_value(selected, index, crate::i18n::tr_label(option));
|
||||
scenario::publish_target(&format!("dialog:{row}:{option}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:{row}:{option}"), response.rect);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -864,7 +909,7 @@ pub(crate) fn dropdown_icons(ui: &mut Ui, id: &str, selected: &mut usize, option
|
||||
for (index, (text, icon)) in options.iter().enumerate() {
|
||||
let response = ui.selectable_label(*selected == index, dropdown_icon_text(text));
|
||||
paint_dropdown_icon(ui, response.rect, icon);
|
||||
scenario::publish_target(&format!("dialog:{row}:{text}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:{row}:{text}"), response.rect);
|
||||
if response.clicked() {
|
||||
*selected = index;
|
||||
}
|
||||
@@ -890,7 +935,7 @@ fn paint_dropdown_icon(ui: &Ui, rect: egui::Rect, icon: &str) {
|
||||
vec2(t.space_16, t.space_16),
|
||||
);
|
||||
crate::icons::paint_ink(ui.painter(), rect, icon, t.form_ink);
|
||||
scenario::publish_rect(&format!("dialog:dropdown_icon:{icon}"), rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:dropdown_icon:{icon}"), rect);
|
||||
}
|
||||
|
||||
/// [`dropdown`] whose options each show a picture beside them while hovered, painted by `preview`
|
||||
@@ -902,10 +947,10 @@ pub(crate) fn dropdown_previews(ui: &mut Ui, id: &str, selected: &mut usize, opt
|
||||
dropdown_menu(ui, id, text, |ui| {
|
||||
for (index, option) in options.iter().enumerate() {
|
||||
let response = ui.selectable_value(selected, index, crate::i18n::tr_label(option));
|
||||
scenario::publish_target(&format!("dialog:{row}:{option}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:{row}:{option}"), response.rect);
|
||||
Container::tooltip_ui(&response, |ui| {
|
||||
preview(ui, index);
|
||||
scenario::publish_rect(&format!("dialog:preview:{row}:{option}"), ui.min_rect());
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:preview:{row}:{option}"), ui.min_rect());
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -921,7 +966,7 @@ pub(crate) fn dropdown_picked(ui: &mut Ui, id: &str, selected: &mut usize, optio
|
||||
for (index, option) in options.iter().enumerate() {
|
||||
let response = ui.selectable_value(selected, index, crate::i18n::tr_label(option));
|
||||
picked |= response.clicked();
|
||||
scenario::publish_handle(&format!("dialog:{row}:{option}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{row}:{option}"), response.rect.center());
|
||||
}
|
||||
});
|
||||
picked
|
||||
@@ -936,11 +981,11 @@ pub(crate) fn dropdown_menu(ui: &mut Ui, id: impl egui::AsIdSalt, current: &str,
|
||||
/// A width-bounded toolbar picker. Long selected values and popup rows truncate;
|
||||
/// callers can retain their complete labels in hover text.
|
||||
pub(crate) fn bounded_dropdown_menu(ui: &mut Ui, id: impl egui::AsIdSalt, current: &str, width: f32, add: impl FnOnce(&mut Ui)) -> Response {
|
||||
crate::prefs_search::note_current(ui, current);
|
||||
if crate::prefs_search::querying(ui)
|
||||
&& (!ui.is_visible() || (crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[current])))
|
||||
super::settings_search::note_current(ui, current);
|
||||
if super::settings_search::querying(ui)
|
||||
&& (!ui.is_visible() || (super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[current])))
|
||||
{
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
let width = dimension(width);
|
||||
let response = ui
|
||||
@@ -1005,13 +1050,13 @@ fn dropdown_menu_width(
|
||||
close_behavior: egui::PopupCloseBehavior,
|
||||
add: impl FnOnce(&mut Ui),
|
||||
) -> Response {
|
||||
crate::prefs_search::note_current(ui, current);
|
||||
super::settings_search::note_current(ui, current);
|
||||
// Only a search probe may skip the real control. A dialog's first frame is not visible yet,
|
||||
// and a stand-in there makes the next frame too short to show OK.
|
||||
if crate::prefs_search::querying(ui)
|
||||
&& (!ui.is_visible() || (crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[current])))
|
||||
if super::settings_search::querying(ui)
|
||||
&& (!ui.is_visible() || (super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[current])))
|
||||
{
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
let t = Tokens::get();
|
||||
let width = if compact {
|
||||
@@ -1106,9 +1151,9 @@ pub(crate) fn selection_field(ui: &mut Ui, text: &str, options: Selection) -> Re
|
||||
false,
|
||||
);
|
||||
publish(ui, &response, egui::Role::Button);
|
||||
scenario::publish_rect(&format!("dialog:field:{}", label(ui)), rect);
|
||||
scenario::publish_count(&format!("dialog:selection:{}:filled", label(ui)), f64::from(filled));
|
||||
scenario::publish_count(&format!("dialog:selection:{}:active", label(ui)), f64::from(active));
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:field:{}", label(ui)), rect);
|
||||
solidcraft_ui_registry::publish_count(&format!("dialog:selection:{}:filled", label(ui)), f64::from(filled));
|
||||
solidcraft_ui_registry::publish_count(&format!("dialog:selection:{}:active", label(ui)), f64::from(active));
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1158,8 +1203,8 @@ pub(crate) fn number_pair<T: egui::emath::Numeric>(
|
||||
/// Retains the established 0.01 step, fractional range and locale parser.
|
||||
/// Locale-aware variable-radius position editor. Tokens: `scope control/font tokens`.
|
||||
pub(crate) fn fraction_field(ui: &mut Ui, value: &mut f64) -> Response {
|
||||
let response = ui.add(egui::DragValue::new(value).speed(0.01).range(0.01..=0.99).fixed_decimals(2).custom_parser(crate::prefs::parse_number));
|
||||
scenario::publish_target(&format!("dialog:fraction:{}", label(ui)), response.rect);
|
||||
let response = ui.add(egui::DragValue::new(value).speed(0.01).range(0.01..=0.99).fixed_decimals(2).custom_parser(parse_number));
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:fraction:{}", label(ui)), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1331,7 +1376,7 @@ pub(crate) fn search_field(ui: &mut Ui, id: egui::Id, value: &mut String, hint:
|
||||
info
|
||||
});
|
||||
if response.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
crate::theme::TabAccent::number_focus(ui, &response);
|
||||
t.input_bevel(ui, &response);
|
||||
@@ -1424,8 +1469,8 @@ impl Default for Slider {
|
||||
/// Native input, keyboard routing, enabled state and accessibility; one token-painted track/thumb.
|
||||
/// Tokens: control_height, space_24/8/4/2, dialog_rule, control_fill, form_primary.
|
||||
pub(crate) fn slider<T: egui::emath::Numeric>(ui: &mut Ui, value: &mut T, range: std::ops::RangeInclusive<T>, options: Slider) -> Response {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
// Own form styling even in callers such as the viewport menu which have no kit scope.
|
||||
super::scope(ui, |ui| {
|
||||
@@ -1513,7 +1558,7 @@ pub(crate) fn slider<T: egui::emath::Numeric>(ui: &mut Ui, value: &mut T, range:
|
||||
ui.ctx().read_response(id).is_some_and(|focused| response.rect.contains(focused.rect.center()))
|
||||
|| egui::TextEdit::load_state(ui.ctx(), id).is_some()
|
||||
}) {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
// Match egui's native input range (handle_radius = thickness / 2.5), so the
|
||||
// painted thumb stays under the pointer even at the two ends of the range.
|
||||
@@ -1534,8 +1579,8 @@ pub(crate) fn slider<T: egui::emath::Numeric>(ui: &mut Ui, value: &mut T, range:
|
||||
ui.painter().circle(center, t.space_8, fill, egui::Stroke::new(t.space_2, if off { t.form_dim } else { primary }));
|
||||
publish(ui, &response, egui::Role::Slider);
|
||||
response.widget_info(|| egui::WidgetInfo::slider(ui.is_enabled(), value.to_f64(), crate::i18n::tr_label(label(ui))));
|
||||
scenario::publish_rect(&format!("dialog:slider_track:{}", label(ui)), slider);
|
||||
scenario::publish_target(&format!("dialog:slider:{}", label(ui)), response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:slider_track:{}", label(ui)), slider);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:slider:{}", label(ui)), response.rect);
|
||||
response
|
||||
})
|
||||
}
|
||||
@@ -1581,7 +1626,7 @@ fn scalar_field_impl(ui: &mut Ui, value: &mut f64, range: std::ops::RangeInclusi
|
||||
let before = *value;
|
||||
let parsed = std::cell::Cell::new(false);
|
||||
let field = egui::DragValue::new(value).range(range).speed(0.01).custom_parser(|text| {
|
||||
let value = crate::prefs::parse_number(text);
|
||||
let value = parse_number(text);
|
||||
parsed.set(value.is_some());
|
||||
value
|
||||
});
|
||||
@@ -1602,7 +1647,7 @@ fn scalar_field_impl(ui: &mut Ui, value: &mut f64, range: std::ops::RangeInclusi
|
||||
if !plain {
|
||||
Tokens::get().input_bevel(ui, &response);
|
||||
}
|
||||
scenario::publish_target(&format!("dialog:scalar:{}", label(ui)), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:scalar:{}", label(ui)), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1611,7 +1656,7 @@ fn scalar_field_impl(ui: &mut Ui, value: &mut f64, range: std::ops::RangeInclusi
|
||||
pub(crate) fn length_field(ui: &mut Ui, value: &mut f64, units: &str, decimals: u8) -> Response {
|
||||
let response = ui.add(length_drag(value, units, decimals));
|
||||
Tokens::get().input_bevel(ui, &response);
|
||||
scenario::publish_target(&format!("dialog:length:{}", label(ui)), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:length:{}", label(ui)), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1625,7 +1670,7 @@ pub(crate) fn bounded_length_field(
|
||||
speed: f64,
|
||||
) -> Response {
|
||||
let response = ui.add(length_drag(value, units, decimals).range(range).speed(speed));
|
||||
scenario::publish_target(&format!("dialog:length:{}", label(ui)), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:length:{}", label(ui)), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1640,13 +1685,13 @@ pub(crate) fn angle_field(ui: &mut Ui, value: &mut f64) -> Response {
|
||||
if let Some(last) = ui.data(|d| d.get_temp::<f64>(set)).filter(|last| (last - shown).abs() <= 1e-9 * last.abs().max(1.0)) {
|
||||
shown = last;
|
||||
}
|
||||
let response = ui.add(egui::DragValue::new(&mut shown).speed(1.0).suffix(" deg").custom_parser(crate::prefs::parse_number));
|
||||
let response = ui.add(egui::DragValue::new(&mut shown).speed(1.0).suffix(" deg").custom_parser(parse_number));
|
||||
if response.changed() {
|
||||
*value = shown;
|
||||
ui.data_mut(|d| d.insert_temp(set, shown));
|
||||
}
|
||||
Tokens::get().input_bevel(ui, &response);
|
||||
scenario::publish_target(&format!("dialog:angle:{}", label(ui)), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:angle:{}", label(ui)), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -1725,15 +1770,10 @@ fn bounded_number_field<T: egui::emath::Numeric>(
|
||||
clamp_existing: bool,
|
||||
) -> Response {
|
||||
let response = ui.add(
|
||||
egui::DragValue::new(value)
|
||||
.range(range)
|
||||
.clamp_existing_to_range(clamp_existing)
|
||||
.speed(speed)
|
||||
.suffix(suffix)
|
||||
.custom_parser(crate::prefs::parse_number),
|
||||
egui::DragValue::new(value).range(range).clamp_existing_to_range(clamp_existing).speed(speed).suffix(suffix).custom_parser(parse_number),
|
||||
);
|
||||
if response.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
Tokens::get().input_bevel(ui, &response);
|
||||
response.widget_info(|| {
|
||||
@@ -1758,7 +1798,7 @@ pub(crate) fn vector3_field(ui: &mut Ui, values: &mut [f64; 3], range: std::ops:
|
||||
ui.spacing_mut().interact_size.x = width;
|
||||
ui.set_clip_rect(ui.clip_rect().intersect(ui.max_rect()));
|
||||
let response = number_field(ui, value, range.clone(), 0.01, suffix);
|
||||
scenario::publish_target(&format!("dialog:vector:{label}:{axis}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:vector:{label}:{axis}"), response.rect);
|
||||
changed |= response.changed();
|
||||
}
|
||||
});
|
||||
@@ -1804,7 +1844,7 @@ pub(crate) fn track_number(ui: &mut Ui, value: &mut f64, speed: f64, decimals: u
|
||||
response
|
||||
}
|
||||
|
||||
/// A value typed as a sum of numbers (`crate::prefs::parse_number` each), added to `now` when it
|
||||
/// A value typed as a sum of numbers (`parse_number` each), added to `now` when it
|
||||
/// starts with a sign: "+30" moves the value by 30, "0-5" is −5.
|
||||
fn relative_number(text: &str, now: f64) -> Option<f64> {
|
||||
let text: String = text.chars().filter(|c| !c.is_whitespace()).map(|c| if c == '\u{2212}' { '-' } else { c }).collect();
|
||||
@@ -1813,13 +1853,13 @@ fn relative_number(text: &str, now: f64) -> Option<f64> {
|
||||
let mut prev = ' ';
|
||||
for c in text.chars() {
|
||||
if (c == '+' || c == '-') && !term.is_empty() && !matches!(prev, 'e' | 'E') {
|
||||
total += crate::prefs::parse_number(&term)?;
|
||||
total += parse_number(&term)?;
|
||||
term.clear();
|
||||
}
|
||||
term.push(c);
|
||||
prev = c;
|
||||
}
|
||||
total += crate::prefs::parse_number(&term)?;
|
||||
total += parse_number(&term)?;
|
||||
total.is_finite().then_some(total)
|
||||
}
|
||||
|
||||
@@ -1931,8 +1971,8 @@ pub(crate) fn library_cell(ui: &mut Ui, id: egui::Id, value: &mut String, hint:
|
||||
let button = egui::Rect::from_min_max(egui::pos2(split, rect.top()), rect.max);
|
||||
t.secondary_vline(ui.painter(), split, rect.y_range(), t.dialog_rule);
|
||||
let library = super::icon_button_at(ui, button, "chevron", "Library");
|
||||
scenario::publish_handle("dialog:menu:Library", button.center());
|
||||
scenario::publish_rect("dialog:menu:Library", button);
|
||||
solidcraft_ui_registry::publish_handle("dialog:menu:Library", button.center());
|
||||
solidcraft_ui_registry::publish_rect("dialog:menu:Library", button);
|
||||
let popup = Container::new(ui.ctx(), Surface::Popup).menu(&library);
|
||||
let open = popup.is_open();
|
||||
popup.show(|ui| super::scope(ui, menu));
|
||||
|
||||
@@ -1419,7 +1419,7 @@ fn a_track_number_adds_up_and_a_sign_adds_to_the_value_now() {
|
||||
|
||||
#[test]
|
||||
fn a_label_drag_reads_only_literal_values_and_moves_them_on_the_step_grid() {
|
||||
let literal = |text: &str| super::literal(text).map(|l| (l.number, l.unit, l.comma));
|
||||
let literal = |text: &str| super::literal(&crate::params_dialog::DocExpressions, text).map(|l| (l.number, l.unit, l.comma));
|
||||
assert_eq!(literal("20 mm"), Some((20.0, " mm".into(), false)));
|
||||
assert_eq!(literal(" 1,5in "), Some((1.5, "in".into(), true)));
|
||||
assert_eq!(literal("-3"), Some((-3.0, String::new(), false)));
|
||||
@@ -1435,7 +1435,7 @@ fn a_label_drag_reads_only_literal_values_and_moves_them_on_the_step_grid() {
|
||||
assert_eq!(super::scrubbed(2.0, -500.0, 0.1), 0.0, "a size stops at zero");
|
||||
assert!((super::scrubbed(-2.0, -10.0, 0.1) + 3.0).abs() < 1e-9, "a negative offset keeps moving");
|
||||
assert_eq!(super::scrubbed(1.0, f32::MAX, 1e8), 1.0, "an overflowing drag keeps the value");
|
||||
let text = |n, step, typed: &str| super::scrub_text(n, step, &super::literal(typed).unwrap());
|
||||
let text = |n, step, typed: &str| super::scrub_text(n, step, &super::literal(&crate::params_dialog::DocExpressions, typed).unwrap());
|
||||
assert_eq!(text(23.0, 0.1, "20 mm"), "23.0 mm");
|
||||
assert_eq!(text(1.75, 0.01, "1,5 in"), "1,75 in");
|
||||
assert_eq!(text(-0.0001, 0.01, "-1"), "0.00");
|
||||
@@ -1473,6 +1473,7 @@ fn dragging_a_value_rows_label_changes_its_literal_and_leaves_expressions_alone(
|
||||
frame(ctx, vec![], modifiers, values);
|
||||
}
|
||||
let ctx = crate::fonts::test_context();
|
||||
crate::params_dialog::install_expressions(&ctx);
|
||||
let mut values = ["20 mm".to_owned(), "depth + 2 mm".to_owned()];
|
||||
frame(&ctx, vec![], Default::default(), &mut values);
|
||||
drag(&ctx, "Distance", 40.0, Default::default(), &mut values);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Shared aspect choices and camera composition guides; no document or command authority.
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Rect, Stroke, Ui, pos2, vec2};
|
||||
|
||||
pub(crate) struct AspectTile<'a> {
|
||||
@@ -48,7 +48,7 @@ pub(crate) fn aspect_grid(ui: &mut Ui, tiles: &[AspectTile<'_>]) -> Option<usize
|
||||
ink,
|
||||
);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), tile.selected, tile.label));
|
||||
scenario::publish_target(tile.id, rect);
|
||||
solidcraft_ui_registry::publish_target(tile.id, rect);
|
||||
if response.clicked() {
|
||||
clicked = Some(row * 3 + column);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Table-driven layout/paint references in both themes. No window or GPU required.
|
||||
use super::preferences_figures::{ThemeSample, paint_theme_thumbnail};
|
||||
use super::*;
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use egui::{Ui, vec2};
|
||||
use solidcraft_engine::kernel::Continuity;
|
||||
|
||||
@@ -29,7 +29,7 @@ macro_rules! element {
|
||||
};
|
||||
}
|
||||
|
||||
fn navigation_sample(ui: &mut Ui, size: crate::navigation_bar::Size) {
|
||||
fn navigation_sample(ui: &mut Ui, size: NavigationSize) {
|
||||
let metrics = NavigationMetrics::for_size(size);
|
||||
let inset = NAV_PADDING + NAV_BORDER;
|
||||
let menu_width = metrics.icon + NAV_MENU_LEFT + NAV_MENU_RIGHT + NAV_MENU_GAP + NAV_CHEVRON;
|
||||
@@ -38,16 +38,12 @@ fn navigation_sample(ui: &mut Ui, size: crate::navigation_bar::Size) {
|
||||
navigation_surface(ui, rect, size, true);
|
||||
let mut x = rect.left() + inset;
|
||||
for (id, icon, kind, selected, width) in [
|
||||
("pan", "pan", crate::navigation_bar::Kind::Tool, true, metrics.button),
|
||||
("fit", "fit", crate::navigation_bar::Kind::Action, false, metrics.button),
|
||||
("visual_style", "style_shaded_edges", crate::navigation_bar::Kind::Menu, false, menu_width),
|
||||
("pan", "pan", NavigationKind::Tool, true, metrics.button),
|
||||
("fit", "fit", NavigationKind::Action, false, metrics.button),
|
||||
("visual_style", "style_shaded_edges", NavigationKind::Menu, false, menu_width),
|
||||
] {
|
||||
let bounds = egui::Rect::from_min_size(egui::pos2(x, rect.top() + inset), vec2(width, metrics.button));
|
||||
navigation_button(
|
||||
ui,
|
||||
bounds,
|
||||
NavigationButton { id, icon, label: id, kind, selected, labels: false, menu: kind == crate::navigation_bar::Kind::Menu },
|
||||
);
|
||||
navigation_button(ui, bounds, NavigationButton { id, icon, label: id, kind, selected, labels: false, menu: kind == NavigationKind::Menu });
|
||||
if id == "fit" {
|
||||
navigation_edit_outline(ui, bounds, true, false);
|
||||
}
|
||||
@@ -220,8 +216,8 @@ const ELEMENTS: &[Element] = &[
|
||||
element!("look_chain_empty", |ui, _| {
|
||||
look_chain(ui, "gallery:empty-chain", ["Face", "Body", "Component", "Material"], [None; 4], LookStep::Face);
|
||||
}),
|
||||
element!("navigation_regular", |ui, _| navigation_sample(ui, crate::navigation_bar::Size::Regular)),
|
||||
element!("navigation_compact", |ui, _| navigation_sample(ui, crate::navigation_bar::Size::Compact)),
|
||||
element!("navigation_regular", |ui, _| navigation_sample(ui, NavigationSize::Regular)),
|
||||
element!("navigation_compact", |ui, _| navigation_sample(ui, NavigationSize::Compact)),
|
||||
element!("navigation_popup", |ui, _| {
|
||||
navigation_menu(ui, NAV_POPUP_WIDTH, |ui| {
|
||||
navigation_menu_heading(ui, "Visual style");
|
||||
@@ -512,7 +508,7 @@ const ELEMENTS: &[Element] = &[
|
||||
element!("catalog_footer_height", |ui, _| {
|
||||
let expected = catalog_footer_height(ui);
|
||||
catalog_footer(ui, "Click to apply to the selection.");
|
||||
assert_eq!(crate::scenario::rect_of("catalog:footer").unwrap().height(), expected);
|
||||
assert_eq!(solidcraft_ui_registry::rect_of("catalog:footer").unwrap().height(), expected);
|
||||
}),
|
||||
element!("catalog_footer_with_tip", |ui, _| {
|
||||
catalog_footer_with_tip(ui, "Click to apply to the selection.", "Paint and drag assignment are not supported yet.");
|
||||
@@ -1216,7 +1212,7 @@ const ELEMENTS: &[Element] = &[
|
||||
let (r, _) = ui.allocate_exact_size(vec2(360.0, 360.0), egui::Sense::hover());
|
||||
let mut app = crate::test_support::sample_app();
|
||||
app.ui.tab = "SURFACE".into();
|
||||
crate::theme::TabAccent::publish(&app, ui.ctx());
|
||||
app.publish_tab_accent(ui.ctx());
|
||||
radial_menu(ui, r.center(), &crate::context_menu::radial_items(&app), Some(3));
|
||||
}),
|
||||
element!("workspace_menu", |ui, _| {
|
||||
@@ -1922,7 +1918,7 @@ const ELEMENTS: &[Element] = &[
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(t.form_button_height * 3.0, t.form_button_height * 2.0), egui::Sense::click());
|
||||
ui.painter().add(persistent_button_shape(ui, &response, rect));
|
||||
ui.painter().text(rect.center(), egui::Align2::CENTER_CENTER, "Selected", crate::fonts::semibold(t.font_label), t.selected_ink);
|
||||
scenario::publish_target("gallery:persistent_button", rect);
|
||||
solidcraft_ui_registry::publish_target("gallery:persistent_button", rect);
|
||||
}),
|
||||
element!("native_button_focus", "gallery:native_button_focus", Some("gallery:native_button_focus"), |ui, _| {
|
||||
let t = Tokens::get();
|
||||
@@ -1930,7 +1926,7 @@ const ELEMENTS: &[Element] = &[
|
||||
response.request_focus();
|
||||
ui.painter().rect_filled(rect, t.radius_sm, t.control_fill);
|
||||
ui.painter().add(native_button_focus(ui, &response, rect));
|
||||
scenario::publish_target("gallery:native_button_focus", rect);
|
||||
solidcraft_ui_registry::publish_target("gallery:native_button_focus", rect);
|
||||
}),
|
||||
element!("button", "dialog:row:Gallery", Some("dialog:button:Gallery:Add"), |ui, _| {
|
||||
row(ui, "Gallery", |ui| {
|
||||
@@ -2094,7 +2090,7 @@ fn example(ui: &mut Ui, element: &Element) -> (&'static str, Option<&'static str
|
||||
// actual shadow bounds as part of the gallery sample, keeping the paint guard strict.
|
||||
ui.expand_to_include_rect(rect + Menu::frame().shadow.margin());
|
||||
}
|
||||
scenario::publish_rect("gallery:allocation", rect);
|
||||
solidcraft_ui_registry::publish_rect("gallery:allocation", rect);
|
||||
(element.rect, element.handle)
|
||||
}
|
||||
|
||||
@@ -2109,11 +2105,11 @@ fn every_element_has_geometry_and_stays_in_its_allocation_in_both_themes() {
|
||||
ctx.set_visuals(if dark { egui::Visuals::dark() } else { egui::Visuals::light() });
|
||||
for pass in 0..3 {
|
||||
let events = if element.name.starts_with("checkbox_hovered") && pass > 0 {
|
||||
vec![egui::Event::PointerMoved(scenario::rect_of("gallery:allocation").expect("hover allocation").center())]
|
||||
vec![egui::Event::PointerMoved(solidcraft_ui_registry::rect_of("gallery:allocation").expect("hover allocation").center())]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
scenario::clear_handles();
|
||||
solidcraft_ui_registry::clear_handles();
|
||||
let mut allocation = egui::Rect::NOTHING;
|
||||
let mut published = ("", None);
|
||||
let mut output = ctx.run_ui(
|
||||
@@ -2135,12 +2131,12 @@ fn every_element_has_geometry_and_stays_in_its_allocation_in_both_themes() {
|
||||
},
|
||||
);
|
||||
output.textures_delta.clear();
|
||||
let rect = scenario::rect_of(published.0).unwrap_or_else(|| panic!("{name}: missing {}", published.0));
|
||||
let rect = solidcraft_ui_registry::rect_of(published.0).unwrap_or_else(|| panic!("{name}: missing {}", published.0));
|
||||
assert!(rect.is_finite() && rect.is_positive(), "{name}: {rect:?}");
|
||||
assert!(allocation.expand(1.0).contains_rect(rect), "{name}: {rect:?} outside {allocation:?}");
|
||||
if let Some(handle) = published.1 {
|
||||
assert!(
|
||||
rect.contains(scenario::handle_at(handle).unwrap_or_else(|| panic!("{name}: missing {handle}"))),
|
||||
rect.contains(solidcraft_ui_registry::handle_at(handle).unwrap_or_else(|| panic!("{name}: missing {handle}"))),
|
||||
"{name}: handle outside {rect:?}"
|
||||
);
|
||||
}
|
||||
@@ -2180,26 +2176,27 @@ fn visual_style_dropdown_specimen_selects_real_options_and_paints_their_icons()
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
}
|
||||
for (index, (_, label)) in crate::navigation_bar::VISUAL_STYLES.into_iter().enumerate() {
|
||||
let trigger = scenario::handle_at("dialog:Visual style").unwrap();
|
||||
let trigger = solidcraft_ui_registry::handle_at("dialog:Visual style").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(trigger, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(trigger, false), &mut draw);
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
for (option, (_, name)) in crate::navigation_bar::VISUAL_STYLES.into_iter().enumerate() {
|
||||
let rect = scenario::rect_of(&format!("dialog:Visual style:{name}")).unwrap();
|
||||
let rect = solidcraft_ui_registry::rect_of(&format!("dialog:Visual style:{name}")).unwrap();
|
||||
let icon = crate::navigation_bar::visual_style_icon(option as u8);
|
||||
let painted = scenario::rect_of(&format!("dialog:dropdown_icon:{icon}")).unwrap();
|
||||
let owner = if option == sample.borrow().selected { scenario::rect_of("dialog:widget:Visual style").unwrap() } else { rect };
|
||||
let painted = solidcraft_ui_registry::rect_of(&format!("dialog:dropdown_icon:{icon}")).unwrap();
|
||||
let owner =
|
||||
if option == sample.borrow().selected { solidcraft_ui_registry::rect_of("dialog:widget:Visual style").unwrap() } else { rect };
|
||||
assert!(owner.contains_rect(painted), "{name}: its real icon is in its option or selected trigger");
|
||||
}
|
||||
let option = scenario::handle_at(&format!("dialog:Visual style:{label}")).unwrap();
|
||||
let option = solidcraft_ui_registry::handle_at(&format!("dialog:Visual style:{label}")).unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(option, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(option, false), &mut draw);
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
assert_eq!(sample.borrow().selected, index, "the specimen edits the real selection index");
|
||||
let icon = crate::navigation_bar::visual_style_icon(index as u8);
|
||||
let trigger = scenario::rect_of("dialog:widget:Visual style").unwrap();
|
||||
assert!(trigger.contains_rect(scenario::rect_of(&format!("dialog:dropdown_icon:{icon}")).unwrap()));
|
||||
assert!(scenario::handle_at("dialog:Visual style:Wireframe").is_none(), "the native popup closes after selection");
|
||||
let trigger = solidcraft_ui_registry::rect_of("dialog:widget:Visual style").unwrap();
|
||||
assert!(trigger.contains_rect(solidcraft_ui_registry::rect_of(&format!("dialog:dropdown_icon:{icon}")).unwrap()));
|
||||
assert!(solidcraft_ui_registry::handle_at("dialog:Visual style:Wireframe").is_none(), "the native popup closes after selection");
|
||||
}
|
||||
}
|
||||
crate::theme::set_dark(previous);
|
||||
@@ -2214,7 +2211,7 @@ fn apply_does_not_submit_and_obeys_validation() {
|
||||
action = footer(ui, "OK", "Cancel", enabled, Footer { apply: Some("Apply"), ..Default::default() });
|
||||
};
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
let at = scenario::handle_at("dialog:apply").unwrap();
|
||||
let at = solidcraft_ui_registry::handle_at("dialog:apply").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, false), &mut draw);
|
||||
assert_eq!(action, if enabled { Action::Apply } else { Action::None });
|
||||
@@ -2229,7 +2226,7 @@ fn picked_item_remove_and_paired_values_have_independent_targets() {
|
||||
removed = list_row(ui, "point-1", "Point 1").clicked();
|
||||
};
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
let at = scenario::handle_at("dialog:list:point-1:remove").unwrap();
|
||||
let at = solidcraft_ui_registry::handle_at("dialog:list:point-1:remove").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, false), &mut draw);
|
||||
assert!(removed);
|
||||
@@ -2240,9 +2237,9 @@ fn picked_item_remove_and_paired_values_have_independent_targets() {
|
||||
value_pair(ui, "Position", [&mut x, &mut y], ["X", "Y"], None);
|
||||
};
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
let row = scenario::rect_of("dialog:row:Position").unwrap();
|
||||
let first = scenario::handle_at("dialog:X").unwrap();
|
||||
let second = scenario::handle_at("dialog:Y").unwrap();
|
||||
let row = solidcraft_ui_registry::rect_of("dialog:row:Position").unwrap();
|
||||
let first = solidcraft_ui_registry::handle_at("dialog:X").unwrap();
|
||||
let second = solidcraft_ui_registry::handle_at("dialog:Y").unwrap();
|
||||
assert_eq!(row.height(), Tokens::get().control_height);
|
||||
assert!(row.contains(first) && row.contains(second) && first.x < second.x);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(second, true), &mut draw);
|
||||
@@ -2269,12 +2266,12 @@ fn gallery_rows_share_a_label_column_after_measurement_in_both_themes() {
|
||||
});
|
||||
});
|
||||
}
|
||||
let first = scenario::rect_of("dialog:control:Edges").unwrap();
|
||||
let second = scenario::rect_of("dialog:control:Counterbore depth").unwrap();
|
||||
let first = solidcraft_ui_registry::rect_of("dialog:control:Edges").unwrap();
|
||||
let second = solidcraft_ui_registry::rect_of("dialog:control:Counterbore depth").unwrap();
|
||||
assert_eq!(first.left(), second.left());
|
||||
assert_eq!(first.height(), Tokens::get().control_height);
|
||||
assert_eq!(second.height(), Tokens::get().control_height);
|
||||
let width = scenario::rect_of("dialog:label:Edges").unwrap().width();
|
||||
let width = solidcraft_ui_registry::rect_of("dialog:label:Edges").unwrap().width();
|
||||
assert!(width >= Tokens::get().form_label_width && width <= Tokens::get().form_label_width * 1.6);
|
||||
}
|
||||
crate::theme::set_dark(previous);
|
||||
@@ -2295,14 +2292,14 @@ fn compound_row_actions_preserve_the_original_field_handle() {
|
||||
field(ui, &mut value);
|
||||
},
|
||||
);
|
||||
expected = scenario::handle_at("dialog:Mid Radius");
|
||||
expected = solidcraft_ui_registry::handle_at("dialog:Mid Radius");
|
||||
button(ui, "×", Button::Small);
|
||||
});
|
||||
});
|
||||
});
|
||||
assert_eq!(scenario::handle_at("dialog:Mid Radius"), expected);
|
||||
assert_eq!(solidcraft_ui_registry::handle_at("dialog:Mid Radius"), expected);
|
||||
assert!(expected.is_some());
|
||||
assert_ne!(scenario::handle_at("dialog:button:Mid Radius:×"), expected);
|
||||
assert_ne!(solidcraft_ui_registry::handle_at("dialog:button:Mid Radius:×"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2326,7 +2323,7 @@ fn emphasis_and_compact_modes_keep_independent_actions_in_both_themes() {
|
||||
};
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
for handle in ["dialog:Bold", "dialog:Italic", "dialog:Alignment:Long translated centre"] {
|
||||
let at = scenario::handle_at(handle).unwrap();
|
||||
let at = solidcraft_ui_registry::handle_at(handle).unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, false), &mut draw);
|
||||
}
|
||||
@@ -2359,8 +2356,8 @@ fn table_cells_keep_alignment_fixed_rows_editing_and_action_targets() {
|
||||
});
|
||||
row.cell(|ui| {
|
||||
let response = cell_editor(ui, ui.scope_id().with("expression"), value, "Expression", false);
|
||||
scenario::publish_handle(&format!("gallery:cell:{name}"), response.rect.center());
|
||||
scenario::publish_rect(&format!("gallery:cell:{name}"), response.rect);
|
||||
solidcraft_ui_registry::publish_handle(&format!("gallery:cell:{name}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_rect(&format!("gallery:cell:{name}"), response.rect);
|
||||
});
|
||||
row.cell(|ui| {
|
||||
if button(ui, "Delete", Button::Small).clicked() {
|
||||
@@ -2376,18 +2373,18 @@ fn table_cells_keep_alignment_fixed_rows_editing_and_action_targets() {
|
||||
for _ in 0..3 {
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
}
|
||||
let first_rect = scenario::rect_of("gallery:cell:Width").unwrap();
|
||||
let second_rect = scenario::rect_of("gallery:cell:Height").unwrap();
|
||||
let header = scenario::rect_of("dialog:column:cells:Expression").unwrap();
|
||||
let first_rect = solidcraft_ui_registry::rect_of("gallery:cell:Width").unwrap();
|
||||
let second_rect = solidcraft_ui_registry::rect_of("gallery:cell:Height").unwrap();
|
||||
let header = solidcraft_ui_registry::rect_of("dialog:column:cells:Expression").unwrap();
|
||||
assert_eq!(first_rect.left(), header.left());
|
||||
assert_eq!(second_rect.left(), header.left());
|
||||
assert_eq!(first_rect.height(), Tokens::get().control_height);
|
||||
assert_eq!(second_rect.height(), Tokens::get().control_height);
|
||||
let at = scenario::handle_at("gallery:cell:Height").unwrap();
|
||||
let at = solidcraft_ui_registry::handle_at("gallery:cell:Height").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, false), &mut draw);
|
||||
super::test_support::frame(&ctx, vec![egui::Event::Text("x".into())], &mut draw);
|
||||
let delete = scenario::handle_at("dialog:button:Actions:Delete").unwrap();
|
||||
let delete = solidcraft_ui_registry::handle_at("dialog:button:Actions:Delete").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(delete, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(delete, false), &mut draw);
|
||||
assert_eq!(first, "10 mm");
|
||||
@@ -2418,7 +2415,7 @@ fn popup_suggestions_and_selectable_lists_keep_click_selection() {
|
||||
for _ in 0..3 {
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
}
|
||||
let at = scenario::handle_at("dialog:suggestion:1").unwrap();
|
||||
let at = solidcraft_ui_registry::handle_at("dialog:suggestion:1").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, false), &mut draw);
|
||||
assert_eq!(selected, Some(1));
|
||||
@@ -2431,7 +2428,7 @@ fn popup_suggestions_and_selectable_lists_keep_click_selection() {
|
||||
});
|
||||
};
|
||||
super::test_support::frame(&ctx, vec![], &mut draw);
|
||||
let at = scenario::handle_at("dialog:list:preset").unwrap();
|
||||
let at = solidcraft_ui_registry::handle_at("dialog:list:preset").unwrap();
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, true), &mut draw);
|
||||
super::test_support::frame(&ctx, super::test_support::pointer(at, false), &mut draw);
|
||||
assert!(picked);
|
||||
@@ -2491,8 +2488,11 @@ fn gallery_covers_every_inline_element() {
|
||||
&& matches!(&item.vis, syn::Visibility::Restricted(v) if v.path.is_ident("crate"))
|
||||
{
|
||||
let name = item.sig.ident.to_string();
|
||||
// Inherited labels, state keys and size measurements do not render elements.
|
||||
if !matches!(name.as_str(), "eye_drag_id" | "icon_label_width" | "label" | "placement_field_content_size") {
|
||||
// Inherited labels, state keys and accessors, parsers and size measurements do not render elements.
|
||||
if !matches!(
|
||||
name.as_str(),
|
||||
"eye_drag_id" | "expressions" | "icon_label_width" | "label" | "parse_number" | "placement_field_content_size"
|
||||
) {
|
||||
assert!(names.contains(name.as_str()), "{file}: {name} needs a gallery entry");
|
||||
}
|
||||
}
|
||||
@@ -2681,7 +2681,7 @@ fn command_search_board_in_both_themes() {
|
||||
let mut textures = HashMap::new();
|
||||
let mut image = None;
|
||||
for _ in 0..3 {
|
||||
scenario::clear_handles();
|
||||
solidcraft_ui_registry::clear_handles();
|
||||
let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(bounds), focused: true, ..Default::default() }, |ui| {
|
||||
ui.painter().rect_filled(bounds, 0.0, Tokens::get().panel);
|
||||
crate::shortcut_box::show(&mut app, ui.ctx());
|
||||
@@ -2690,25 +2690,25 @@ fn command_search_board_in_both_themes() {
|
||||
output.textures_delta.clear();
|
||||
image = Some(raster(&ctx, output.shapes, output.pixels_per_point, bounds, &textures));
|
||||
}
|
||||
let popup = scenario::rect_of("ribbon:search_popup").unwrap();
|
||||
let field = scenario::rect_of("shortcut:search").unwrap();
|
||||
let popup = solidcraft_ui_registry::rect_of("ribbon:search_popup").unwrap();
|
||||
let field = solidcraft_ui_registry::rect_of("shortcut:search").unwrap();
|
||||
assert_eq!(field.height(), 40.0);
|
||||
assert_eq!(field.width(), 520.0);
|
||||
assert_eq!(popup.width(), 520.0);
|
||||
assert_eq!(popup.top(), field.bottom() + Tokens::get().space_6);
|
||||
assert_eq!(popup.x_range(), field.x_range());
|
||||
let footer = scenario::rect_of("ribbon:search_footer").unwrap();
|
||||
let footer = solidcraft_ui_registry::rect_of("ribbon:search_footer").unwrap();
|
||||
assert_eq!(footer, egui::Rect::from_min_max(egui::pos2(popup.left(), footer.top()), popup.right_bottom()));
|
||||
assert!(bounds.contains_rect(popup), "{state}: {popup:?}");
|
||||
match state {
|
||||
"matches" => {
|
||||
assert!(scenario::handle_at("menu:solid.fillet").is_some());
|
||||
assert_eq!(scenario::rect_of("menu:solid.fillet").unwrap().height(), 38.0);
|
||||
assert!(scenario::handle_at("menu:search:design:0").is_some());
|
||||
assert!(solidcraft_ui_registry::handle_at("menu:solid.fillet").is_some());
|
||||
assert_eq!(solidcraft_ui_registry::rect_of("menu:solid.fillet").unwrap().height(), 38.0);
|
||||
assert!(solidcraft_ui_registry::handle_at("menu:search:design:0").is_some());
|
||||
}
|
||||
"command" => assert!(scenario::rect_of("dialog:note:command_parameters").is_some()),
|
||||
"invalid" => assert!(scenario::rect_of("dialog:report:command_parameters").is_some()),
|
||||
"no-match" => assert!(scenario::rect_of("dialog:report:search_no_match").is_some()),
|
||||
"command" => assert!(solidcraft_ui_registry::rect_of("dialog:note:command_parameters").is_some()),
|
||||
"invalid" => assert!(solidcraft_ui_registry::rect_of("dialog:report:command_parameters").is_some()),
|
||||
"no-match" => assert!(solidcraft_ui_registry::rect_of("dialog:report:search_no_match").is_some()),
|
||||
_ => {}
|
||||
}
|
||||
let image = image.unwrap();
|
||||
@@ -2773,7 +2773,7 @@ fn confirm_radial_and_history_rows_keep_multitone_artwork() {
|
||||
ctx.set_visuals(if dark { egui::Visuals::dark() } else { egui::Visuals::light() });
|
||||
let mut app = crate::SolveApp::new(Default::default(), Default::default());
|
||||
app.ui.tab = tab.into();
|
||||
crate::theme::TabAccent::publish(&app, &ctx);
|
||||
app.publish_tab_accent(&ctx);
|
||||
let bounds = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(600.0, 360.0));
|
||||
let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(bounds), ..Default::default() }, |ui| {
|
||||
ui.painter().rect_filled(bounds, 0.0, Tokens::get().panel);
|
||||
@@ -2782,7 +2782,7 @@ fn confirm_radial_and_history_rows_keep_multitone_artwork() {
|
||||
radial_menu(
|
||||
ui,
|
||||
egui::pos2(180.0, 180.0),
|
||||
&[crate::context_menu::Item::action("solid.extrude", "Extrude", icon)],
|
||||
&[super::Item::action("solid.extrude", "Extrude", icon)],
|
||||
(surface == "radial_hover").then_some(0),
|
||||
);
|
||||
}
|
||||
@@ -2913,7 +2913,7 @@ fn command_search_ribbon_transition_clips_content_in_both_themes() {
|
||||
app.ui.palette_open = false;
|
||||
app.sbox.text.clear();
|
||||
}
|
||||
scenario::clear_handles();
|
||||
solidcraft_ui_registry::clear_handles();
|
||||
let mut output = ctx.run_ui(
|
||||
egui::RawInput { screen_rect: Some(bounds), time: Some(f64::from(frame) / 60.0), focused: true, ..Default::default() },
|
||||
|ui| {
|
||||
@@ -2923,12 +2923,12 @@ fn command_search_ribbon_transition_clips_content_in_both_themes() {
|
||||
);
|
||||
textures_update(&mut textures, &output.textures_delta);
|
||||
output.textures_delta.clear();
|
||||
let field = scenario::rect_of("ribbon:search").unwrap();
|
||||
let field = solidcraft_ui_registry::rect_of("ribbon:search").unwrap();
|
||||
if frame == 2 {
|
||||
closed = field;
|
||||
}
|
||||
if (3..14).contains(&frame) {
|
||||
let sheet = scenario::rect_of("ribbon:search_popup").unwrap();
|
||||
let sheet = solidcraft_ui_registry::rect_of("ribbon:search_popup").unwrap();
|
||||
assert!(sheet.top() > field.bottom() && sheet.top() <= field.bottom() + Tokens::get().space_6);
|
||||
// Actual result text is painted with the unfolding sheet's clip,
|
||||
// rather than relying on a smaller background hiding overflow.
|
||||
@@ -3021,7 +3021,7 @@ fn file_view_application_evidence_in_both_themes_and_widths() {
|
||||
textures_update(&mut textures, &output.textures_delta);
|
||||
if frame == 5 {
|
||||
assert!(app.home.open);
|
||||
let page = scenario::rect_of("home:page").expect("actual File view allocation");
|
||||
let page = solidcraft_ui_registry::rect_of("home:page").expect("actual File view allocation");
|
||||
assert!(bounds.contains_rect(page));
|
||||
raster(&ctx, output.shapes.clone(), output.pixels_per_point, bounds, &textures)
|
||||
.save(output_dir.join(format!("file-{name}-{}.png", if dark { "dark" } else { "light" })))
|
||||
|
||||
@@ -239,7 +239,7 @@ impl HistoryTransport {
|
||||
pub fn show(self, ui: &mut egui::Ui, rect: Rect, playing: bool) -> egui::Response {
|
||||
let t = Tokens::get();
|
||||
let response = ui.interact(rect, ui.scope_id().with(("history_transport", self.id())), egui::Sense::click());
|
||||
crate::scenario::publish_target(&format!("history:{}", self.id()), rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("history:{}", self.id()), rect);
|
||||
if response.hovered() {
|
||||
ui.painter().rect_filled(rect, t.radius_sm, t.hover);
|
||||
}
|
||||
@@ -399,9 +399,9 @@ pub(crate) fn history_seam(ui: &mut egui::Ui, card: Rect, names: bool) -> Histor
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("ui.history_edge_hint")).size(t.font_small).color(t.text_dim));
|
||||
});
|
||||
}
|
||||
crate::scenario::publish_handle("history:names", seam.pill.center());
|
||||
crate::scenario::publish_rect("history:seam", seam.edge);
|
||||
crate::scenario::publish_rect("history:handle", seam.pill);
|
||||
solidcraft_ui_registry::publish_handle("history:names", seam.pill.center());
|
||||
solidcraft_ui_registry::publish_rect("history:seam", seam.edge);
|
||||
solidcraft_ui_registry::publish_rect("history:handle", seam.pill);
|
||||
// egui's click response includes Enter and Space for a focused button.
|
||||
HistorySeamResponse {
|
||||
toggle: button.clicked() || edge.double_clicked(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Layout elements for dense cards; see `docs/ui-kit.md` for tokens and usage.
|
||||
use super::{metrics::*, text::paint_text};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use egui::{Response, RichText, Ui, vec2};
|
||||
use solidcraft_engine::kernel::Continuity;
|
||||
|
||||
@@ -98,7 +98,7 @@ pub(crate) fn catalog_footer(ui: &mut Ui, text: &str) -> Response {
|
||||
let galley = ui.painter().layout_job(job);
|
||||
ui.painter().galley(rect.left_top() + vec2(t.space_24, t.space_8), galley, t.form_dim);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), text));
|
||||
crate::scenario::publish_rect("catalog:footer", rect);
|
||||
solidcraft_ui_registry::publish_rect("catalog:footer", rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ impl CatalogLayout {
|
||||
.min_scrolled_height(0.0)
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, add);
|
||||
scenario::publish_rect(&format!("{}:body", self.key), scroll.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("{}:body", self.key), scroll.inner_rect);
|
||||
scroll.inner
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ impl CatalogLayout {
|
||||
if (height - self.footer_height).abs() > 0.5 {
|
||||
ui.ctx().request_discard("catalog footer measurement changed");
|
||||
}
|
||||
scenario::publish_rect(&format!("{}:content", self.key), self.bounds);
|
||||
scenario::publish_rect(&format!("{}:footer", self.key), scroll.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("{}:content", self.key), self.bounds);
|
||||
solidcraft_ui_registry::publish_rect(&format!("{}:footer", self.key), scroll.inner_rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ pub(super) fn publish(ui: &Ui, response: &Response, kind: egui::Role) {
|
||||
egui::Role::TextInput => {
|
||||
t.input_bevel(ui, response);
|
||||
if response.has_focus() {
|
||||
crate::prefs_search::note_key_sink(ui.ctx());
|
||||
super::settings_search::note_key_sink(ui.ctx());
|
||||
}
|
||||
}
|
||||
egui::Role::ComboBox | egui::Role::Button => t.button_bevel(ui, response, false),
|
||||
@@ -208,7 +208,7 @@ pub(crate) fn status_line(ui: &mut Ui, text: &str, tone: StatusTone) -> Response
|
||||
StatusTone::Error => t.error,
|
||||
};
|
||||
let response = ui.add(egui::Label::new(RichText::new(crate::i18n::tr_label(text)).size(t.font_label).color(ink)).wrap());
|
||||
scenario::publish_rect(&format!("dialog:status:{text}"), response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:status:{text}"), response.rect);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -299,31 +299,31 @@ fn scrub_row<R>(ui: &mut Ui, text: &str, control: impl FnOnce(&mut Ui, Option<&R
|
||||
|
||||
fn row_with<R>(ui: &mut Ui, text: &str, height: f32, scrub: bool, control: impl FnOnce(&mut Ui, Option<&Response>) -> R) -> R {
|
||||
let mut passthrough = false;
|
||||
if crate::prefs_search::querying(ui) {
|
||||
if super::settings_search::querying(ui) {
|
||||
// A row with no label has no words to match; its controls supply them (#9265).
|
||||
let unlabelled = text.is_empty();
|
||||
if unlabelled {
|
||||
crate::prefs_search::set_unlabelled_control(ui);
|
||||
super::settings_search::set_unlabelled_control(ui);
|
||||
} else {
|
||||
crate::prefs_search::set_control(ui, text);
|
||||
super::settings_search::set_control(ui, text);
|
||||
}
|
||||
if !crate::prefs_search::visible(ui, &[text]) {
|
||||
if !super::settings_search::visible(ui, &[text]) {
|
||||
if unlabelled {
|
||||
crate::prefs_search::push_unlabelled(ui);
|
||||
super::settings_search::push_unlabelled(ui);
|
||||
}
|
||||
let result = crate::prefs_search::run_hidden(ui, text, |ui| control(ui, None));
|
||||
let result = super::settings_search::run_hidden(ui, text, |ui| control(ui, None));
|
||||
if unlabelled {
|
||||
crate::prefs_search::pop_unlabelled(ui);
|
||||
super::settings_search::pop_unlabelled(ui);
|
||||
}
|
||||
crate::prefs_search::recheck(ui, &[text]);
|
||||
super::settings_search::recheck(ui, &[text]);
|
||||
return result;
|
||||
}
|
||||
crate::prefs_search::push_passthrough(ui);
|
||||
super::settings_search::push_passthrough(ui);
|
||||
passthrough = true;
|
||||
}
|
||||
let result = sized_row_body(ui, text, height, scrub, control);
|
||||
if passthrough {
|
||||
crate::prefs_search::pop_passthrough(ui);
|
||||
super::settings_search::pop_passthrough(ui);
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -332,8 +332,8 @@ fn sized_row_body<R>(ui: &mut Ui, text: &str, height: f32, scrub: bool, control:
|
||||
let t = Tokens::get();
|
||||
let width = dimension(ui.available_width());
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, height), egui::Sense::hover());
|
||||
crate::prefs_search::remember(ui, text, rect);
|
||||
crate::prefs_search::follow(ui, text, rect);
|
||||
super::settings_search::remember(ui, text, rect);
|
||||
super::settings_search::follow(ui, text, rect);
|
||||
// A nested row (`" Of"`) lines up under the one above: the lookup uses the text without its
|
||||
// indent and the indent stays in front of the translation. The `LABEL` tag keeps the original.
|
||||
let shown = indented_label(text);
|
||||
@@ -359,8 +359,8 @@ fn sized_row_body<R>(ui: &mut Ui, text: &str, height: f32, scrub: bool, control:
|
||||
)
|
||||
.inner;
|
||||
let help = super::help_icon(ui, help);
|
||||
scenario::publish_rect(&format!("dialog:row_label:{text}"), label.rect);
|
||||
scenario::publish_rect(&format!("dialog:row_help:{text}"), help.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:row_label:{text}"), label.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:row_help:{text}"), help.rect);
|
||||
} else {
|
||||
ui.add(egui::Label::new(RichText::new(shown.clone()).size(t.font_label).color(t.dialog_label)).truncate())
|
||||
.backing_on_hover_text(shown);
|
||||
@@ -388,10 +388,10 @@ fn sized_row_body<R>(ui: &mut Ui, text: &str, height: f32, scrub: bool, control:
|
||||
},
|
||||
)
|
||||
.inner;
|
||||
scenario::publish_rect(&format!("dialog:row:{text}"), rect);
|
||||
scenario::publish_rect(&format!("dialog:label:{text}"), label_rect);
|
||||
scenario::publish_rect(&format!("dialog:control:{text}"), control_rect);
|
||||
scenario::publish_handle(&format!("dialog:row:{text}"), rect.center());
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:row:{text}"), rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:label:{text}"), label_rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:control:{text}"), control_rect);
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:row:{text}"), rect.center());
|
||||
result
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ pub(crate) fn warning_caption(ui: &mut Ui, aligned_to: &str, text: &str) {
|
||||
}
|
||||
|
||||
fn caption_in(ui: &mut Ui, aligned_to: &str, text: &str, color: egui::Color32) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -418,7 +418,7 @@ fn caption_in(ui: &mut Ui, aligned_to: &str, text: &str, color: egui::Color32) {
|
||||
let left = (rect.left() + label_column(ui, aligned_to, width) + label_gap(ui, width)).min(rect.right());
|
||||
let text_rect = egui::Rect::from_min_max(egui::pos2(left, rect.top()), rect.max);
|
||||
paint_text(ui, text_rect, text, egui::FontId::proportional(t.font_small), color, false);
|
||||
scenario::publish_rect(&format!("dialog:caption:{aligned_to}"), text_rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:caption:{aligned_to}"), text_rect);
|
||||
}
|
||||
|
||||
/// Natural-height content aligned beneath an indexed row's control, bounded independently of the form.
|
||||
@@ -485,8 +485,8 @@ pub(crate) fn figure_rows(
|
||||
top += slot + t.space_4;
|
||||
}
|
||||
paint(ui.painter(), figure, &slots);
|
||||
scenario::publish_rect(&format!("dialog:figure:{id}"), rect);
|
||||
scenario::publish_rect(&format!("dialog:figure_picture:{id}"), figure);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:figure:{id}"), rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:figure_picture:{id}"), figure);
|
||||
enter
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ pub(crate) fn section_heading(ui: &mut Ui, title: &str) {
|
||||
let t = Tokens::get();
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(dimension(ui.available_width()), t.space_16), egui::Sense::hover());
|
||||
paint_text(ui, rect, &crate::i18n::tr_label(title).to_uppercase(), crate::fonts::semibold(t.font_caption), t.form_dim, true);
|
||||
scenario::publish_rect(&format!("dialog:heading:{title}"), rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:heading:{title}"), rect);
|
||||
}
|
||||
|
||||
pub(crate) fn section<R>(ui: &mut Ui, id: impl egui::AsIdSalt, title: &str, default_open: bool, add: impl FnOnce(&mut Ui) -> R) -> Option<R> {
|
||||
@@ -570,10 +570,10 @@ fn section_with_badge<R>(
|
||||
let (count, pending, compact) = badge;
|
||||
// A search still walks the body, so a setting inside a folded section can match, without
|
||||
// leaving the folded heading on screen for every query.
|
||||
if crate::prefs_search::querying(ui) && crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[title]) {
|
||||
if super::settings_search::querying(ui) && super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[title]) {
|
||||
return Some(add(ui));
|
||||
}
|
||||
let searching = crate::prefs_search::querying(ui);
|
||||
let searching = super::settings_search::querying(ui);
|
||||
let t = Tokens::get();
|
||||
ui.add_space(t.space_2);
|
||||
let id = ui.make_persistent_id(id);
|
||||
@@ -591,7 +591,7 @@ fn section_with_badge<R>(
|
||||
),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
crate::prefs_search::follow(ui, title, rect);
|
||||
super::settings_search::follow(ui, title, rect);
|
||||
if response.hovered() || response.has_focus() {
|
||||
ui.painter().rect_filled(rect.shrink2(vec2(0.0, 1.0)), t.radius_sm, t.hover);
|
||||
}
|
||||
@@ -629,7 +629,7 @@ fn section_with_badge<R>(
|
||||
egui::FontId::proportional(t.font_small),
|
||||
t.form_dim,
|
||||
);
|
||||
scenario::publish_rect(&format!("dialog:section:{title}:count"), badge);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:section:{title}:count"), badge);
|
||||
if pending {
|
||||
rule_right = badge.left() - t.space_12;
|
||||
} else {
|
||||
@@ -642,8 +642,8 @@ fn section_with_badge<R>(
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::DisclosureTriangle, ui.is_enabled(), open, crate::i18n::tr_label(title)));
|
||||
// A disclosure says whether it is open as expanded, as egui's own collapsing header does.
|
||||
ui.ctx().accesskit_node_builder(response.id, |node| node.set_expanded(open));
|
||||
scenario::publish_handle(&format!("dialog:section:{title}"), response.rect.center());
|
||||
scenario::publish_rect(&format!("dialog:section:{title}"), rect);
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:section:{title}"), response.rect.center());
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:section:{title}"), rect);
|
||||
if response.clicked() && !pending && !searching {
|
||||
open = !open;
|
||||
}
|
||||
@@ -666,13 +666,13 @@ fn section_with_badge<R>(
|
||||
pub(crate) fn search_fold<R>(ui: &mut Ui, title: &str, count: usize, add: impl FnOnce(&mut Ui) -> R) -> Option<R> {
|
||||
let t = Tokens::get();
|
||||
let id = egui::Id::unique(("prefs_search_fold", title));
|
||||
let query = crate::prefs_search::query(ui);
|
||||
let query = super::settings_search::query(ui);
|
||||
let mut open = ui.data(|data| data.get_temp::<bool>(id)).unwrap_or(false);
|
||||
let previous = ui.data(|data| data.get_temp::<String>(id.with("query"))).unwrap_or_default();
|
||||
if previous != query {
|
||||
open = false;
|
||||
}
|
||||
crate::prefs_search::reveal(ui);
|
||||
super::settings_search::reveal(ui);
|
||||
ui.add_space(t.space_2);
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(dimension(ui.available_width()), row_height()), egui::Sense::click());
|
||||
if response.hovered() || response.has_focus() {
|
||||
@@ -717,8 +717,8 @@ pub(crate) fn search_fold<R>(ui: &mut Ui, title: &str, count: usize, add: impl F
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::DisclosureTriangle, ui.is_enabled(), open, &accessible));
|
||||
// A disclosure says whether it is open as expanded, as egui's own collapsing header does.
|
||||
ui.ctx().accesskit_node_builder(response.id, |node| node.set_expanded(open));
|
||||
scenario::publish_rect("prefs:search:shortcuts", rect);
|
||||
scenario::publish_handle("prefs:search:shortcuts", response.rect.center());
|
||||
solidcraft_ui_registry::publish_rect("prefs:search:shortcuts", rect);
|
||||
solidcraft_ui_registry::publish_handle("prefs:search:shortcuts", response.rect.center());
|
||||
if response.clicked() {
|
||||
open = !open;
|
||||
}
|
||||
@@ -727,9 +727,9 @@ pub(crate) fn search_fold<R>(ui: &mut Ui, title: &str, count: usize, add: impl F
|
||||
data.insert_temp(id, open);
|
||||
});
|
||||
open.then(|| {
|
||||
crate::prefs_search::push_force(ui);
|
||||
super::settings_search::push_force(ui);
|
||||
let result = add(ui);
|
||||
crate::prefs_search::pop_force(ui);
|
||||
super::settings_search::pop_force(ui);
|
||||
result
|
||||
})
|
||||
}
|
||||
@@ -745,20 +745,20 @@ pub(crate) fn list_row(ui: &mut Ui, key: &str, text: &str) -> Response {
|
||||
let label = egui::Rect::from_min_max(rect.min, egui::pos2((remove.left() - t.space_8).max(rect.left()), rect.bottom()));
|
||||
paint_text(ui, label, text, egui::FontId::proportional(t.font_label), t.form_ink, false);
|
||||
let response = ui.push_id(key, |ui| super::icon_button_at(ui, remove, "close", "Remove")).inner;
|
||||
scenario::publish_rect(&format!("dialog:list:{key}"), rect);
|
||||
scenario::publish_handle(&format!("dialog:list:{key}:remove"), remove.center());
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:list:{key}"), rect);
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:list:{key}:remove"), remove.center());
|
||||
response.union(row)
|
||||
}
|
||||
|
||||
/// Full-width rule; Tokens: dialog_rule. Use between groups without extra padding.
|
||||
/// Full-width rule without extra padding. Tokens: `dialog_rule`.
|
||||
pub(crate) fn divider(ui: &mut Ui, key: &str) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return;
|
||||
}
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(dimension(ui.available_width()), 1.0), egui::Sense::hover());
|
||||
Tokens::get().secondary_hline(ui.painter(), rect.x_range(), rect.top(), Tokens::get().dialog_rule);
|
||||
scenario::publish_rect(&format!("dialog:divider:{key}"), rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:divider:{key}"), rect);
|
||||
}
|
||||
|
||||
/// Give a compound row's subfield its accessible name without allocating layout space.
|
||||
@@ -771,12 +771,12 @@ pub(crate) fn named_control<R>(ui: &mut Ui, label: &str, add: impl FnOnce(&mut U
|
||||
/// Publish geometry without replacing native accessibility. Tokens: `none`.
|
||||
pub(super) fn publish_geometry(ui: &Ui, response: &Response) {
|
||||
let label = label(ui);
|
||||
scenario::publish_rect(&format!("dialog:widget:{label}"), response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:widget:{label}"), response.rect);
|
||||
// A click lands where the field shows: a field scrolled out of a dialog's field area, under
|
||||
// its footer, has no handle (a click at its centre would press the button over it).
|
||||
let shown = response.rect.intersect(ui.clip_rect());
|
||||
if shown.is_positive() {
|
||||
scenario::publish_handle(&format!("dialog:{label}"), shown.center());
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:{label}"), shown.center());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +797,7 @@ pub(crate) fn text_lines(ui: &mut Ui, id: &str, text: &str) {
|
||||
ui.add(egui::Label::new(RichText::new(*line).font(font.clone()).color(t.form_ink)).truncate()).backing_on_hover_text(*line);
|
||||
}
|
||||
});
|
||||
scenario::publish_rect(&format!("dialog:lines:{id}"), response.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:lines:{id}"), response.inner_rect);
|
||||
}
|
||||
|
||||
/// Read-only text variants retain their original wrapping, allocation and scenario identities.
|
||||
@@ -837,20 +837,20 @@ pub(crate) fn name_cell_with_hint(ui: &mut Ui, name: &str, strong: bool, hint: &
|
||||
}
|
||||
|
||||
fn text_impl(ui: &mut Ui, text: &str, style: Text<'_>, hover_hint: Option<&str>) -> Response {
|
||||
if crate::prefs_search::querying(ui) {
|
||||
if super::settings_search::querying(ui) {
|
||||
let heading = matches!(style, Text::Heading(_));
|
||||
if heading {
|
||||
let hit = crate::prefs_search::texts_hit(ui, &[text]);
|
||||
crate::prefs_search::set_heading_open(ui, hit);
|
||||
let hit = super::settings_search::texts_hit(ui, &[text]);
|
||||
super::settings_search::set_heading_open(ui, hit);
|
||||
if hit {
|
||||
crate::prefs_search::reveal(ui);
|
||||
} else if crate::prefs_search::suppress_chrome(ui) {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
super::settings_search::reveal(ui);
|
||||
} else if super::settings_search::suppress_chrome(ui) {
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
} else if crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[text]) {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
} else if crate::prefs_search::texts_hit(ui, &[text]) && !crate::prefs_search::shows_all(ui) {
|
||||
crate::prefs_search::reveal(ui);
|
||||
} else if super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[text]) {
|
||||
return super::settings_search::hidden_response(ui);
|
||||
} else if super::settings_search::texts_hit(ui, &[text]) && !super::settings_search::shows_all(ui) {
|
||||
super::settings_search::reveal(ui);
|
||||
}
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -922,13 +922,13 @@ fn text_impl(ui: &mut Ui, text: &str, style: Text<'_>, hover_hint: Option<&str>)
|
||||
Text::Cell | Text::MonoCell | Text::NameCell(_) | Text::SelectedNameCell => ("cell", text),
|
||||
};
|
||||
let id = format!("dialog:{kind}:{key}");
|
||||
scenario::publish_rect(&id, response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&id, response.rect);
|
||||
if matches!(style, Text::Heading(_)) {
|
||||
crate::prefs_search::remember(ui, text, response.rect);
|
||||
crate::prefs_search::follow(ui, text, response.rect);
|
||||
super::settings_search::remember(ui, text, response.rect);
|
||||
super::settings_search::follow(ui, text, response.rect);
|
||||
}
|
||||
if !matches!(style, Text::Note(_) | Text::MonoNote(_)) {
|
||||
scenario::publish_handle(&id, response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle(&id, response.rect.center());
|
||||
}
|
||||
response
|
||||
}
|
||||
@@ -956,8 +956,8 @@ pub(crate) fn summary_row(ui: &mut Ui, name: &str, summary: &str, enabled: &mut
|
||||
paint_text(ui, detail, summary, egui::FontId::proportional(t.font_small), t.form_dim, false);
|
||||
let response = ui.interact(text, ui.scope_id().with("summary"), egui::Sense::hover()).backing_on_hover_text(summary);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), format!("{name}: {summary}")));
|
||||
scenario::publish_rect(&format!("dialog:inline:{name}"), title);
|
||||
scenario::publish_rect("dialog:report:rule_report", detail);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:inline:{name}"), title);
|
||||
solidcraft_ui_registry::publish_rect("dialog:report:rule_report", detail);
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(egui::Rect::from_min_max(egui::pos2(right, rect.top()), rect.max))
|
||||
|
||||
@@ -262,7 +262,7 @@ fn mouse_mapping_search_keeps_main_gesture_chords_and_actions() {
|
||||
let output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
|
||||
scope(ui, |ui| {
|
||||
ui.set_width(280.0);
|
||||
let _search = crate::prefs_search::begin(ui, query);
|
||||
let _search = crate::ui_kit::settings_search::begin(ui, query);
|
||||
settings_mouse_mapping(
|
||||
ui,
|
||||
&[(vec!["Ctrl/Cmd".into(), "Middle drag".into()], "Zoom toward the cursor".into()), (vec!["Alt/Option".into()], "Orbit".into())],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Appearance inheritance selector. The caller owns selection and commands.
|
||||
use super::{Sphere, sphere::paint_sphere};
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Rect, Sense, Stroke, Ui, pos2, vec2};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -79,10 +79,10 @@ pub fn look_chain(ui: &mut Ui, id: &str, labels: [&str; 4], looks: [Option<Spher
|
||||
let label_left = (cell.center().x - label_width / 2.0).max(cell.left());
|
||||
let label_rect = Rect::from_min_max(pos2(label_left, cell.top() + side + t.space_2), cell.max);
|
||||
super::text::paint_text(ui, label_rect, label, font, if step == active { t.render_accent } else { t.form_dim }, false);
|
||||
scenario::publish_rect(&key, cell);
|
||||
scenario::publish_handle(&key, cell.center());
|
||||
solidcraft_ui_registry::publish_rect(&key, cell);
|
||||
solidcraft_ui_registry::publish_handle(&key, cell.center());
|
||||
}
|
||||
scenario::publish_rect(id, rect);
|
||||
solidcraft_ui_registry::publish_rect(id, rect);
|
||||
clicked
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::scenario;
|
||||
use crate::ui_kit::{
|
||||
FinishKind,
|
||||
test_support::{frame, pointer},
|
||||
|
||||
@@ -1,7 +1,104 @@
|
||||
//! Menus share one popup surface and one row painter. Command dispatch stays with callers.
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::{context_menu::Item, fonts::RichTextExt as _, icons, theme::Tokens};
|
||||
use crate::{fonts::RichTextExt as _, icons, theme::Tokens};
|
||||
use egui::{Color32, FontId, Rect, Response, Sense, Stroke, Ui, pos2, vec2};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
/// One menu entry: a command id (run with `params` when given, else started like a toolbar
|
||||
/// click) or a `ui.*` action. `-` is a separator, `#…` a section heading.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub shortcut: String,
|
||||
pub enabled: bool,
|
||||
pub icon: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub params: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub children: Vec<Item>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub swatch: Option<[u8; 3]>,
|
||||
/// Why the entry is off: shown on hover, and in the control channel's report.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tip: Option<String>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// A view action (`ui.*`).
|
||||
pub fn action(id: &str, label: &str, icon: &str) -> Item {
|
||||
Item {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
shortcut: String::new(),
|
||||
enabled: true,
|
||||
icon: icon.into(),
|
||||
params: None,
|
||||
children: Vec::new(),
|
||||
swatch: None,
|
||||
tip: None,
|
||||
}
|
||||
}
|
||||
pub fn sep() -> Item {
|
||||
Item {
|
||||
id: "-".into(),
|
||||
label: String::new(),
|
||||
shortcut: String::new(),
|
||||
enabled: false,
|
||||
icon: String::new(),
|
||||
params: None,
|
||||
children: Vec::new(),
|
||||
swatch: None,
|
||||
tip: None,
|
||||
}
|
||||
}
|
||||
pub(crate) fn heading(label: &str) -> Item {
|
||||
Item {
|
||||
id: format!("#{label}"),
|
||||
label: label.into(),
|
||||
shortcut: String::new(),
|
||||
enabled: false,
|
||||
icon: String::new(),
|
||||
params: None,
|
||||
children: Vec::new(),
|
||||
swatch: None,
|
||||
tip: None,
|
||||
}
|
||||
}
|
||||
pub(crate) fn is_sep(&self) -> bool {
|
||||
self.id == "-"
|
||||
}
|
||||
pub(crate) fn is_heading(&self) -> bool {
|
||||
self.id.starts_with('#')
|
||||
}
|
||||
pub fn with(mut self, params: Value) -> Item {
|
||||
self.params = Some(params);
|
||||
self
|
||||
}
|
||||
pub fn on_if(self, enabled: bool) -> Item {
|
||||
self.on(enabled)
|
||||
}
|
||||
pub(crate) fn on(mut self, enabled: bool) -> Item {
|
||||
self.enabled &= enabled;
|
||||
self
|
||||
}
|
||||
/// Off, saying why, when there is a reason.
|
||||
pub(crate) fn off_because(mut self, why: Option<String>) -> Item {
|
||||
if let Some(why) = why {
|
||||
self.enabled = false;
|
||||
self.tip = Some(why);
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn key(mut self, k: &str) -> Item {
|
||||
self.shortcut = k.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of list menus.
|
||||
pub const LIST_W: f32 = 250.0;
|
||||
|
||||
pub(crate) struct Menu;
|
||||
|
||||
@@ -244,7 +341,7 @@ impl Menu {
|
||||
leftward: bool,
|
||||
add: impl FnOnce(&mut Ui) -> R,
|
||||
) -> egui::InnerResponse<R> {
|
||||
let width = crate::context_menu::LIST_W;
|
||||
let width = LIST_W;
|
||||
let screen = ctx.content_rect();
|
||||
// The estimate only places the area's first, invisible sizing pass.
|
||||
let height = ctx.memory(|m| m.area_rect(id)).map_or(height, |r| r.height());
|
||||
@@ -362,7 +459,7 @@ impl<'a> MenuItem<'a> {
|
||||
let galley = painter.layout_job(job);
|
||||
painter.galley(pos2(left, r.center().y - galley.size().y / 2.0), galley, ink);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, label));
|
||||
crate::scenario::publish_target(&format!("menu:{}", self.id), r);
|
||||
solidcraft_ui_registry::publish_target(&format!("menu:{}", self.id), r);
|
||||
if enabled && response.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
@@ -408,7 +505,7 @@ pub(crate) fn workspace_menu(ui: &mut egui::Ui, items: &[Item], key_row: Option<
|
||||
if item.enabled { Sense::click() } else { Sense::hover() },
|
||||
);
|
||||
for handle in [format!("menu:{}", item.id), format!("workspace:menu:{slug}")] {
|
||||
crate::scenario::publish_target(&handle, r);
|
||||
solidcraft_ui_registry::publish_target(&handle, r);
|
||||
}
|
||||
if current || (item.enabled && (resp.hovered() || key_row == Some(k))) {
|
||||
ui.painter().rect_filled(r, t.control_radius.saturating_sub(1), soft);
|
||||
@@ -497,7 +594,7 @@ pub(crate) fn rename_menu(ui: &mut Ui, value: &mut String, focused: &mut bool) -
|
||||
let id = egui::Id::unique("sc_rename_text");
|
||||
let response = ui.add(super::fields::text_input(egui::TextEdit::singleline(value).id(id)).desired_width(ui.available_width()));
|
||||
t.input_bevel(ui, &response);
|
||||
crate::scenario::publish_target("rename:name", response.rect);
|
||||
solidcraft_ui_registry::publish_target("rename:name", response.rect);
|
||||
if !*focused {
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), id) {
|
||||
state.cursor.set_char_range(Some(egui::text::CCursorRange::two(
|
||||
|
||||
@@ -42,9 +42,11 @@ mod preferences_layout;
|
||||
mod preferences_navigation;
|
||||
mod preferences_small;
|
||||
mod radial;
|
||||
pub(crate) mod recent_colors;
|
||||
mod reports;
|
||||
mod review;
|
||||
mod search;
|
||||
pub(crate) mod settings_search;
|
||||
mod shortcuts;
|
||||
mod sphere;
|
||||
mod stages;
|
||||
@@ -56,6 +58,7 @@ mod theme_picker;
|
||||
mod tree;
|
||||
mod value_focus;
|
||||
mod viewport_status;
|
||||
mod wheel;
|
||||
|
||||
pub(crate) use actions::*;
|
||||
pub use actions::{Button, button};
|
||||
@@ -71,26 +74,29 @@ pub(crate) use choice::*;
|
||||
pub(crate) use color::*;
|
||||
pub(crate) use compare::*;
|
||||
pub(crate) use confirm::{Confirm, Tone};
|
||||
pub use container::{BackingTooltipExt, Container, DockEdges, Edge, Surface};
|
||||
pub use container::{BackingTooltipExt, Container, DockEdges, Edge, LayoutMode, Surface};
|
||||
pub(crate) use container::{SHELL_GAP, input_blocked, set_input_blocked};
|
||||
pub(crate) use details::*;
|
||||
pub(crate) use disclosure::*;
|
||||
pub(crate) use drag::*;
|
||||
pub(crate) use fields::*;
|
||||
pub use fields::{cell_editor, wide_cell_editor};
|
||||
pub use fields::{Expressions, cell_editor, install_expressions, wide_cell_editor};
|
||||
pub(crate) use file_view::{FileView, RecentRow, RecoveryMessage, SampleCard};
|
||||
pub(crate) use framing::*;
|
||||
pub(crate) use history::*;
|
||||
pub(crate) use history_group::*;
|
||||
pub(crate) use layout::*;
|
||||
pub use look_chain::{LookStep, look_chain};
|
||||
pub use menu::{Item, LIST_W};
|
||||
pub(crate) use menu::{Menu, MenuItem, rename_menu, workspace_menu};
|
||||
pub(crate) use metrics::{ContentWidth, header_height, row_height, table_row_height};
|
||||
pub use name_list::{NameList, NameRow, name_list};
|
||||
pub(crate) use navigation::*;
|
||||
pub use navigation::{NavigationEdge, NavigationIdle, NavigationSize};
|
||||
pub(crate) use navigation_menu::*;
|
||||
pub(crate) use navigation_settings::*;
|
||||
pub(crate) use panel::{
|
||||
ComputeStatus, FeatureOptions, InspectorAction, Panel, Part, WindowPage, WorkspaceOptions, inspector_header, panel_header_hover,
|
||||
ComputeStatus, FeatureOptions, InspectorAction, Panel, Part, WindowPage, WorkspaceOptions, inspector_header, panel_header_hover, within,
|
||||
};
|
||||
pub use panel::{DialogButtons, Window};
|
||||
#[cfg(test)]
|
||||
@@ -116,7 +122,7 @@ pub(crate) use shortcuts::*;
|
||||
pub(crate) use sphere::sphere_preview;
|
||||
pub use sphere::{FinishKind, Sphere, sphere_thumbnail};
|
||||
pub(crate) use sphere::{SphereTile, sphere_cursor, sphere_grid, sphere_grid_drag, sphere_preview_with_source};
|
||||
pub(crate) use stages::{StageAction, StageBar};
|
||||
pub(crate) use stages::{Cards, StageAction, StageBar};
|
||||
pub(crate) use status::*;
|
||||
pub(crate) use table::*;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! cells leave. Built on the kit's table grid; see `docs/ui-kit.md` for tokens and usage.
|
||||
use super::metrics::dimension;
|
||||
use super::table::{Column, Table, TableRow, table_custom};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use egui::{Response, Ui, vec2};
|
||||
|
||||
/// Columns past this are ignored: a list of named rows has a handful of cells.
|
||||
@@ -111,7 +111,7 @@ fn name_text(ui: &mut Ui, name: &str) -> Response {
|
||||
super::text::paint_text(ui, rect, name, font, t.form_ink, false);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), name));
|
||||
let id = format!("dialog:name:{name}");
|
||||
scenario::publish_rect(&id, rect);
|
||||
scenario::publish_count(&format!("{id}:cut"), f64::from(u8::from(cut)));
|
||||
solidcraft_ui_registry::publish_rect(&id, rect);
|
||||
solidcraft_ui_registry::publish_count(&format!("{id}:cut"), f64::from(u8::from(cut)));
|
||||
if cut { response.backing_on_hover_text(name) } else { response }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
//! Navigation chrome and measurements shared by the viewport and Preferences editor.
|
||||
use crate::{
|
||||
icons,
|
||||
navigation_bar::{Idle, Kind, Size},
|
||||
theme::Tokens,
|
||||
};
|
||||
use crate::{icons, theme::Tokens};
|
||||
use egui::{Rect, Response, Sense, Ui, pos2, vec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(crate) const NAV_BORDER: f32 = 1.0;
|
||||
pub(crate) const NAV_PADDING: f32 = 3.0;
|
||||
@@ -41,6 +38,48 @@ pub(crate) const NAV_FADE_SECONDS: f32 = 0.12;
|
||||
pub(crate) const NAV_TOOLTIP_DELAY: f64 = 0.8;
|
||||
pub(crate) const NAV_TOOLTIP_KEY_LINE_HEIGHT: f32 = 15.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum NavigationKind {
|
||||
Action,
|
||||
Tool,
|
||||
Toggle,
|
||||
Menu,
|
||||
Separator,
|
||||
}
|
||||
|
||||
/// The edge of the 3D view the bar sits on (#8183): the bottom, or the top, under the ribbon.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NavigationEdge {
|
||||
#[default]
|
||||
Bottom,
|
||||
Top,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NavigationSize {
|
||||
Compact,
|
||||
#[default]
|
||||
Regular,
|
||||
}
|
||||
|
||||
impl NavigationSize {
|
||||
// Logical points: egui's UI zoom applies the preference's scale to these dimensions.
|
||||
pub(crate) fn points(self) -> f32 {
|
||||
NavigationMetrics::for_size(self).button
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NavigationIdle {
|
||||
Always,
|
||||
#[default]
|
||||
Fade,
|
||||
Hide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct NavigationMetrics {
|
||||
pub button: f32,
|
||||
@@ -49,10 +88,10 @@ pub(crate) struct NavigationMetrics {
|
||||
pub button_radius: u8,
|
||||
}
|
||||
impl NavigationMetrics {
|
||||
pub(crate) fn for_size(size: Size) -> Self {
|
||||
pub(crate) fn for_size(size: NavigationSize) -> Self {
|
||||
match size {
|
||||
Size::Regular => Self { button: 32.0, icon: 18.0, radius: 10, button_radius: 7 },
|
||||
Size::Compact => Self { button: 26.0, icon: 16.0, radius: 9, button_radius: 6 },
|
||||
NavigationSize::Regular => Self { button: 32.0, icon: 18.0, radius: 10, button_radius: 7 },
|
||||
NavigationSize::Compact => Self { button: 26.0, icon: 16.0, radius: 9, button_radius: 6 },
|
||||
}
|
||||
}
|
||||
pub(crate) fn height(self) -> f32 {
|
||||
@@ -65,7 +104,7 @@ pub(crate) struct NavigationButton<'a> {
|
||||
pub label: &'a str,
|
||||
pub icon: &'a str,
|
||||
pub selected: bool,
|
||||
pub kind: Kind,
|
||||
pub kind: NavigationKind,
|
||||
pub labels: bool,
|
||||
pub menu: bool,
|
||||
}
|
||||
@@ -79,10 +118,10 @@ pub(crate) enum NavigationFill {
|
||||
}
|
||||
|
||||
/// Actions never latch, and keyboard focus has its own outline rather than a fill.
|
||||
pub(crate) fn navigation_visual(kind: Kind, active: bool, hovered: bool, pressed: bool, keyboard_focus: bool) -> (NavigationFill, bool) {
|
||||
let fill = if pressed || (active && kind == Kind::Menu) {
|
||||
pub(crate) fn navigation_visual(kind: NavigationKind, active: bool, hovered: bool, pressed: bool, keyboard_focus: bool) -> (NavigationFill, bool) {
|
||||
let fill = if pressed || (active && kind == NavigationKind::Menu) {
|
||||
NavigationFill::Press
|
||||
} else if active && matches!(kind, Kind::Tool | Kind::Toggle) {
|
||||
} else if active && matches!(kind, NavigationKind::Tool | NavigationKind::Toggle) {
|
||||
NavigationFill::Active
|
||||
} else if hovered {
|
||||
NavigationFill::Hover
|
||||
@@ -114,12 +153,12 @@ pub(crate) fn navigation_ink(t: &Tokens, fill: NavigationFill) -> egui::Color32
|
||||
/// WCAG small-text contrast for an active slot's label on its selected fill.
|
||||
pub(crate) const NAV_ACTIVE_MIN_CONTRAST: f64 = 4.5;
|
||||
|
||||
pub(crate) fn navigation_opacity(idle: Idle, active: bool) -> f32 {
|
||||
pub(crate) fn navigation_opacity(idle: NavigationIdle, active: bool) -> f32 {
|
||||
match idle {
|
||||
_ if active => 1.0,
|
||||
Idle::Always => 1.0,
|
||||
Idle::Fade => NAV_IDLE_OPACITY,
|
||||
Idle::Hide => 0.0,
|
||||
NavigationIdle::Always => 1.0,
|
||||
NavigationIdle::Fade => NAV_IDLE_OPACITY,
|
||||
NavigationIdle::Hide => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +168,11 @@ pub(crate) fn navigation_button(ui: &mut Ui, rect: Rect, button: NavigationButto
|
||||
|
||||
pub(crate) fn navigation_button_editable(ui: &mut Ui, rect: Rect, button: NavigationButton<'_>, editing: bool) -> Response {
|
||||
let t = Tokens::get();
|
||||
let metrics =
|
||||
NavigationMetrics::for_size(if rect.height() < NavigationMetrics::for_size(Size::Regular).button { Size::Compact } else { Size::Regular });
|
||||
let metrics = NavigationMetrics::for_size(if rect.height() < NavigationMetrics::for_size(NavigationSize::Regular).button {
|
||||
NavigationSize::Compact
|
||||
} else {
|
||||
NavigationSize::Regular
|
||||
});
|
||||
let response = ui.interact(rect, ui.make_persistent_id(("nav_slot", button.id)), if editing { Sense::click_and_drag() } else { Sense::click() });
|
||||
let pressed = response.is_pointer_button_down_on();
|
||||
if pressed || response.clicked_by(egui::PointerButton::Primary) {
|
||||
@@ -191,8 +233,11 @@ pub(crate) fn navigation_separator(ui: &Ui, rect: Rect) {
|
||||
|
||||
pub(crate) fn navigation_edit_outline(ui: &Ui, rect: Rect, hidden: bool, selected: bool) {
|
||||
let t = Tokens::get();
|
||||
let metrics =
|
||||
NavigationMetrics::for_size(if rect.height() < NavigationMetrics::for_size(Size::Regular).button { Size::Compact } else { Size::Regular });
|
||||
let metrics = NavigationMetrics::for_size(if rect.height() < NavigationMetrics::for_size(NavigationSize::Regular).button {
|
||||
NavigationSize::Compact
|
||||
} else {
|
||||
NavigationSize::Regular
|
||||
});
|
||||
let mut painter = ui.painter().clone();
|
||||
if hidden {
|
||||
painter.multiply_opacity(NAV_HIDDEN_OPACITY);
|
||||
@@ -219,7 +264,7 @@ pub(crate) fn navigation_edit_outline(ui: &Ui, rect: Rect, hidden: bool, selecte
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn navigation_surface(ui: &Ui, rect: Rect, size: Size, editing: bool) {
|
||||
pub(crate) fn navigation_surface(ui: &Ui, rect: Rect, size: NavigationSize, editing: bool) {
|
||||
let t = Tokens::get();
|
||||
let frame = super::Container::new(ui.ctx(), super::Surface::Navigation)
|
||||
.frame(ui.ctx(), t.dialog_body, crate::frame::Side::Left)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Popup and tooltip skin from the approved navigation boards.
|
||||
use super::{Container, Surface, navigation::*};
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{icons, navigation_bar::Edge, theme::Tokens};
|
||||
use crate::{icons, theme::Tokens};
|
||||
use egui::{Align2, FontId, Rect, Response, Sense, Ui, pos2, vec2};
|
||||
|
||||
pub(crate) fn navigation_popup_frame() -> egui::Frame {
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn navigation_menu_separator(ui: &mut Ui) {
|
||||
|
||||
/// A tip beyond `along` (the bar, or the row it names): above it, or below it for a bar on the
|
||||
/// view's top edge.
|
||||
pub(crate) fn navigation_tooltip(response: &Response, along: Rect, edge: Edge, label: &str, shortcut: Option<&str>) {
|
||||
pub(crate) fn navigation_tooltip(response: &Response, along: Rect, edge: NavigationEdge, label: &str, shortcut: Option<&str>) {
|
||||
let ctx = &response.ctx;
|
||||
let id = response.id.with("nav_tooltip_delay");
|
||||
if !response.hovered() {
|
||||
@@ -130,10 +130,10 @@ pub(crate) fn navigation_tooltip(response: &Response, along: Rect, edge: Edge, l
|
||||
.area(response.id.with("nav_tooltip"))
|
||||
.order(egui::Order::Tooltip)
|
||||
.interactable(false)
|
||||
.pivot(if edge == Edge::Top { Align2::CENTER_TOP } else { Align2::CENTER_BOTTOM })
|
||||
.pivot(if edge == NavigationEdge::Top { Align2::CENTER_TOP } else { Align2::CENTER_BOTTOM })
|
||||
.fixed_pos(match edge {
|
||||
Edge::Bottom => pos2(response.rect.center().x, along.top() - NAV_POPUP_GAP),
|
||||
Edge::Top => pos2(response.rect.center().x, along.bottom() + NAV_POPUP_GAP),
|
||||
NavigationEdge::Bottom => pos2(response.rect.center().x, along.top() - NAV_POPUP_GAP),
|
||||
NavigationEdge::Top => pos2(response.rect.center().x, along.bottom() + NAV_POPUP_GAP),
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
Container::new(ctx, Surface::Popup).show_frame(ui, frame, ui.max_rect(), |ui| {
|
||||
|
||||
@@ -128,7 +128,7 @@ pub(crate) fn navigation_anchor_picker(ui: &mut Ui, id: &str, selected: usize, l
|
||||
);
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::RadioButton, ui.is_enabled(), on, *label));
|
||||
crate::scenario::publish_target(&format!("prefs:navbar:anchor:{index}"), cell);
|
||||
solidcraft_ui_registry::publish_target(&format!("prefs:navbar:anchor:{index}"), cell);
|
||||
if response.backing_on_hover_text(*label).clicked() {
|
||||
clicked = Some(index);
|
||||
}
|
||||
@@ -241,7 +241,7 @@ pub(crate) fn navigation_tile(ui: &mut Ui, id: &str, label: &str, icon: &str, dr
|
||||
(response, add)
|
||||
}
|
||||
|
||||
pub(crate) fn navigation_drag_ghost(ui: &Ui, at: egui::Pos2, button: NavigationButton<'_>, size: crate::navigation_bar::Size) {
|
||||
pub(crate) fn navigation_drag_ghost(ui: &Ui, at: egui::Pos2, button: NavigationButton<'_>, size: NavigationSize) {
|
||||
let ctx = ui.ctx();
|
||||
let t = Tokens::get();
|
||||
let metrics = NavigationMetrics::for_size(size);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
//! Panel elements for dense cards; see `docs/ui-kit.md` for tokens and usage.
|
||||
use super::{actions::icon_button_at, metrics::*, text::paint_text};
|
||||
use crate::theme::Tokens;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{scenario, theme::Tokens};
|
||||
use egui::{Ui, vec2};
|
||||
|
||||
const WINDOW_PAGES_NARROW_WIDTH: f32 = 150.0;
|
||||
const WINDOW_PAGES_NARROW_AT: f32 = 700.0;
|
||||
|
||||
/// `v` kept within `lo..=hi`, or `lo` when that range is empty: `f32::clamp` panics on
|
||||
/// `lo > hi`, which a rect squeezed to nothing between cards gives.
|
||||
pub(crate) fn within(v: f32, lo: f32, hi: f32) -> f32 {
|
||||
if lo <= hi { v.clamp(lo, hi) } else { lo }
|
||||
}
|
||||
|
||||
/// Shared active-tab hover tint for the browser and collapsible panel chrome.
|
||||
pub(crate) fn panel_header_hover(painter: &egui::Painter, rect: egui::Rect) {
|
||||
let t = Tokens::get();
|
||||
@@ -64,7 +70,7 @@ fn header_actions(
|
||||
);
|
||||
if closeable {
|
||||
let close = icon_button_at(ui, close_rect, "close", "Close");
|
||||
scenario::publish_handle("dialog:close", close.rect.center());
|
||||
solidcraft_ui_registry::publish_handle("dialog:close", close.rect.center());
|
||||
if close.clicked() {
|
||||
on_close();
|
||||
}
|
||||
@@ -83,12 +89,12 @@ fn header_actions(
|
||||
action_ui.set_clip_rect(ui.clip_rect().intersect(action_rect));
|
||||
}
|
||||
actions(&mut action_ui);
|
||||
scenario::publish_rect("dialog:header:accessory", action_rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:header:accessory", action_rect);
|
||||
action_rect.left() - t.space_8
|
||||
} else if let Some(collapsed) = collapsed {
|
||||
let fold = egui::Rect::from_min_size(egui::pos2(close_rect.left() - t.form_close_size - t.space_4, close_rect.top()), close_rect.size());
|
||||
let response = super::disclosure_button_at(ui, fold, !*collapsed, crate::i18n::tr_label(if *collapsed { "Expand" } else { "Collapse" }));
|
||||
scenario::publish_handle("dialog:collapse", response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle("dialog:collapse", response.rect.center());
|
||||
if response.clicked() {
|
||||
*collapsed = !*collapsed;
|
||||
}
|
||||
@@ -103,7 +109,7 @@ fn header_actions(
|
||||
paint_text(ui, title_rect, title, crate::fonts::semibold(if icon.0.is_empty() { t.font_label } else { t.font_heading }), t.form_ink, false);
|
||||
let title_response = ui.interact(title_rect, ui.make_persistent_id("title_text"), egui::Sense::hover());
|
||||
title_response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), title));
|
||||
scenario::publish_rect("dialog:header", rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:header", rect);
|
||||
t.secondary_hline(ui.painter(), rect.x_range(), rect.bottom(), t.dialog_rule);
|
||||
}
|
||||
|
||||
@@ -166,11 +172,11 @@ impl<'a> TopHeader<'a> {
|
||||
// left out: one control for one action, and its width goes to the title. Its handle
|
||||
// moves to Cancel, so automation that closes a panel keeps working.
|
||||
if let Some(cancel_rect) = actions.rect(super::Action::Cancel) {
|
||||
scenario::publish_handle("dialog:close", cancel_rect.center());
|
||||
solidcraft_ui_registry::publish_handle("dialog:close", cancel_rect.center());
|
||||
} else {
|
||||
let close_rect = target(right);
|
||||
let response = icon_button_at(ui, close_rect, "close", "Close");
|
||||
scenario::publish_handle("dialog:close", response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle("dialog:close", response.rect.center());
|
||||
if response.clicked() {
|
||||
*close = true;
|
||||
}
|
||||
@@ -179,7 +185,7 @@ impl<'a> TopHeader<'a> {
|
||||
if let Some(collapsed) = collapsed {
|
||||
let fold = target(right);
|
||||
let response = super::disclosure_button_at(ui, fold, !*collapsed, crate::i18n::tr_label(if *collapsed { "Expand" } else { "Collapse" }));
|
||||
scenario::publish_handle("dialog:collapse", response.rect.center());
|
||||
solidcraft_ui_registry::publish_handle("dialog:collapse", response.rect.center());
|
||||
if response.clicked() {
|
||||
*collapsed = !*collapsed;
|
||||
}
|
||||
@@ -192,8 +198,8 @@ impl<'a> TopHeader<'a> {
|
||||
let left = (icon.right() + t.space_8).min(right);
|
||||
let title = egui::Rect::from_min_max(egui::pos2(left, band.top()), egui::pos2(right.max(left), band.bottom()));
|
||||
// Dragging the panel by its title must not start on a button.
|
||||
scenario::publish_handle("dialog:title", title.center());
|
||||
scenario::publish_rect("dialog:header", band);
|
||||
solidcraft_ui_registry::publish_handle("dialog:title", title.center());
|
||||
solidcraft_ui_registry::publish_rect("dialog:header", band);
|
||||
t.secondary_hline(ui.painter(), band.x_range(), band.bottom(), t.dialog_rule);
|
||||
Self { icon, title, actions }
|
||||
}
|
||||
@@ -243,7 +249,7 @@ pub(crate) fn inspector_header(ui: &mut Ui, icon: &str, title: &str, kind: &str)
|
||||
vec2(t.form_close_size, t.form_close_size),
|
||||
);
|
||||
let close = super::icon_button_at(ui, close_rect, "close", crate::i18n::tr_label("Close"));
|
||||
scenario::publish_target("render:appearance:close", close.rect);
|
||||
solidcraft_ui_registry::publish_target("render:appearance:close", close.rect);
|
||||
if close.clicked() {
|
||||
action = Some(InspectorAction::Close);
|
||||
}
|
||||
@@ -272,7 +278,7 @@ pub(crate) fn inspector_header(ui: &mut Ui, icon: &str, title: &str, kind: &str)
|
||||
ui.interact(name, ui.make_persistent_id("inspector_title"), egui::Sense::hover())
|
||||
.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), title));
|
||||
t.secondary_hline(ui.painter(), rect.x_range(), rect.bottom(), t.dialog_rule);
|
||||
scenario::publish_rect("dialog:header", rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:header", rect);
|
||||
action
|
||||
}
|
||||
|
||||
@@ -376,7 +382,7 @@ impl<'a> Panel<'a> {
|
||||
let band = ui.max_rect().expand2(vec2(t.space_12, t.space_8));
|
||||
ui.painter().rect_filled(band, egui::CornerRadius { nw: 0, ne: 0, sw: t.radius_xl, se: t.radius_xl }, t.dialog_footer);
|
||||
t.secondary_hline(ui.painter(), band.x_range(), band.top(), t.dialog_rule);
|
||||
scenario::publish_rect("dialog:editor_footer", band);
|
||||
solidcraft_ui_registry::publish_rect("dialog:editor_footer", band);
|
||||
add(ui)
|
||||
})
|
||||
}
|
||||
@@ -523,18 +529,18 @@ impl<'a> Panel<'a> {
|
||||
let grip = ui
|
||||
.interact(grip_rect, ui.scope_id().with("workspace_grip"), egui::Sense::click_and_drag())
|
||||
.on_hover_cursor(egui::CursorIcon::Grab);
|
||||
scenario::publish_target(&format!("{}:grip", options.id), grip.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:grip", options.id), grip.rect);
|
||||
super::Menu::context(&grip, |ui| {
|
||||
let (target, label) = if left { ("right", "Move to right side") } else { ("left", "Move to left side") };
|
||||
let item = super::MenuItem::new("view.render_dock", label, "").show(ui);
|
||||
scenario::publish_target(&format!("{}:move_side", options.id), item.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:move_side", options.id), item.rect);
|
||||
if item.clicked() {
|
||||
move_side = Some(target);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
header(ui, &options);
|
||||
scenario::publish_rect(&format!("{}:header", options.id), band);
|
||||
solidcraft_ui_registry::publish_rect(&format!("{}:header", options.id), band);
|
||||
if grip.dragged() {
|
||||
let frame_nr = ui.ctx().cumulative_frame_nr();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::unique("sc_dock_drag_frame"), frame_nr));
|
||||
@@ -575,7 +581,7 @@ impl<'a> Panel<'a> {
|
||||
);
|
||||
let resize =
|
||||
ui.interact(rail, ui.scope_id().with("workspace_resize"), egui::Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
|
||||
scenario::publish_target(&format!("{}:resize", options.id), rail);
|
||||
solidcraft_ui_registry::publish_target(&format!("{}:resize", options.id), rail);
|
||||
if resize.dragged() && !options.compact {
|
||||
let delta = ui.input(|i| i.pointer.delta().x) * if left { 1.0 } else { -1.0 };
|
||||
if delta.is_finite() {
|
||||
@@ -594,12 +600,12 @@ impl<'a> Panel<'a> {
|
||||
}
|
||||
|
||||
/// Feature surface; shared container policy supplies the border and shadow.
|
||||
pub(crate) fn feature_frame(ctx: &egui::Context, layout: crate::shell::LayoutMode, side: crate::frame::Side) -> egui::Frame {
|
||||
pub(crate) fn feature_frame(ctx: &egui::Context, layout: super::LayoutMode, side: crate::frame::Side) -> egui::Frame {
|
||||
super::Container::for_layout(layout, super::Surface::Feature).frame(ctx, Tokens::get().dialog_body, side).inner_margin(0.0)
|
||||
}
|
||||
|
||||
/// Sketch palette uses the same layout policy as every other workspace panel.
|
||||
pub(crate) fn palette_frame(ctx: &egui::Context, layout: crate::shell::LayoutMode) -> egui::Frame {
|
||||
pub(crate) fn palette_frame(ctx: &egui::Context, layout: super::LayoutMode) -> egui::Frame {
|
||||
let t = Tokens::get();
|
||||
super::Container::for_layout(layout, super::Surface::Palette)
|
||||
.frame(ctx, t.panel.gamma_multiply(t.sketch_palette_alpha), crate::frame::Side::Right)
|
||||
@@ -680,7 +686,7 @@ impl<'a> Panel<'a> {
|
||||
viewport.left() + width,
|
||||
(viewport.right() - if gap > 0.0 { Tokens::get().space_8 } else { 0.0 }).max(viewport.left() + width),
|
||||
),
|
||||
crate::frame::Side::Left => crate::shell::within(self.anchor.x, viewport.left(), viewport.right() - width),
|
||||
crate::frame::Side::Left => within(self.anchor.x, viewport.left(), viewport.right() - width),
|
||||
};
|
||||
let t = Tokens::get();
|
||||
let chrome = t.form_header_height + t.form_footer_height + t.form_gap_y * 2.0 + self.frame.total_margin().sum().y + gap;
|
||||
@@ -792,7 +798,7 @@ impl<'a> Panel<'a> {
|
||||
let min = ui.cursor().min;
|
||||
let band = egui::Rect::from_min_size(min, vec2(ui.available_width(), super::header_height()));
|
||||
if heading.drag_key.is_empty() {
|
||||
scenario::publish_handle("dialog:title", band.center());
|
||||
solidcraft_ui_registry::publish_handle("dialog:title", band.center());
|
||||
} else {
|
||||
crate::frame::drag_dock(ui, band, heading.drag_key, heading.moved, self.bounded_anchor(ui.ctx()) - heading.docked);
|
||||
}
|
||||
@@ -816,7 +822,7 @@ impl<'a> Panel<'a> {
|
||||
let fields_height = self.body_rows.map_or(fields_height, |rows| fields_height.min(rows * t.control_height + t.space_12 * 2.0));
|
||||
let fields_rect = egui::Rect::from_min_size(ui.cursor().min, vec2(ui.available_width(), fields_height));
|
||||
let state = if collapsed.as_deref().copied().unwrap_or(false) {
|
||||
scenario::publish_count("dialog:body:content_height", 0.0);
|
||||
solidcraft_ui_registry::publish_count("dialog:body:content_height", 0.0);
|
||||
ComputeStatus { text: None, busy: disabled, applying: disabled }
|
||||
} else {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(fields_rect), |ui| {
|
||||
@@ -839,9 +845,9 @@ impl<'a> Panel<'a> {
|
||||
.show(ui, body)
|
||||
.inner
|
||||
});
|
||||
scenario::publish_count("dialog:body:content_height", f64::from(scroll.content_size.y));
|
||||
scenario::publish_count("dialog:body:scroll_y", f64::from(scroll.state.clamped_offset().y));
|
||||
scenario::publish_rect("dialog:body:viewport", scroll.inner_rect);
|
||||
solidcraft_ui_registry::publish_count("dialog:body:content_height", f64::from(scroll.content_size.y));
|
||||
solidcraft_ui_registry::publish_count("dialog:body:scroll_y", f64::from(scroll.state.clamped_offset().y));
|
||||
solidcraft_ui_registry::publish_rect("dialog:body:viewport", scroll.inner_rect);
|
||||
scroll.inner
|
||||
})
|
||||
.inner
|
||||
@@ -864,7 +870,7 @@ impl<'a> Panel<'a> {
|
||||
if state.busy {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(t.space_16, ui.available_height()), egui::Sense::hover());
|
||||
egui::Spinner::new().size(t.space_14).paint_at(ui, rect);
|
||||
scenario::publish_rect("dialog:footer:spinner", rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:footer:spinner", rect);
|
||||
let status = state.text.as_deref().unwrap_or_else(|| crate::i18n::tr_label("Updating the preview"));
|
||||
ui.interact(rect, ui.scope_id().with("compute_status"), egui::Sense::hover()).backing_on_hover_text(status);
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
|
||||
@@ -915,7 +921,7 @@ impl<'a> Panel<'a> {
|
||||
.show(ui, body)
|
||||
})
|
||||
.inner;
|
||||
scenario::publish_rect("sketch_palette:options", scroll.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect("sketch_palette:options", scroll.inner_rect);
|
||||
}
|
||||
if !at_top {
|
||||
add(ui, Part::Footer);
|
||||
@@ -928,7 +934,7 @@ impl<'a> Panel<'a> {
|
||||
.min_scrolled_height(0.0)
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, contents);
|
||||
scenario::publish_rect("sketch_palette:options", scroll.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect("sketch_palette:options", scroll.inner_rect);
|
||||
} else {
|
||||
contents(ui);
|
||||
}
|
||||
@@ -1254,7 +1260,7 @@ impl<'a> Window<'a> {
|
||||
super::Container::finish_content(ui, frame);
|
||||
result
|
||||
});
|
||||
scenario::publish_rect("dialog:window", shown.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:window", shown.response.rect);
|
||||
return shown.inner;
|
||||
}
|
||||
let window = super::Container::new(ctx, super::Surface::Window)
|
||||
@@ -1281,7 +1287,7 @@ impl<'a> Window<'a> {
|
||||
result
|
||||
});
|
||||
if let Some(shown) = &shown {
|
||||
scenario::publish_rect("dialog:window", shown.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:window", shown.response.rect);
|
||||
}
|
||||
shown.and_then(|r| r.inner).unwrap_or((false, super::Action::None))
|
||||
}
|
||||
@@ -1330,7 +1336,7 @@ fn page_list_sized<S>(
|
||||
let nav = egui::Rect::from_min_size(body.min, vec2(width, body.height()));
|
||||
ui.painter().rect_filled(nav, 0.0, t.dialog_footer);
|
||||
t.secondary_vline(ui.painter(), nav.right(), nav.y_range(), t.dialog_rule);
|
||||
scenario::publish_rect("dialog:pages", nav);
|
||||
solidcraft_ui_registry::publish_rect("dialog:pages", nav);
|
||||
*selected = (*selected).min(pages.len().saturating_sub(1));
|
||||
let nav_inner = if sidebar_footer.is_some() {
|
||||
egui::Rect::from_min_max(nav.min + vec2(t.space_8, t.space_6), nav.max - vec2(t.space_8, t.space_10))
|
||||
@@ -1389,7 +1395,7 @@ fn page_list_sized<S>(
|
||||
if response.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
scenario::publish_target(page.handle, rect);
|
||||
solidcraft_ui_registry::publish_target(page.handle, rect);
|
||||
response.backing_on_hover_text(label);
|
||||
}
|
||||
});
|
||||
@@ -1400,7 +1406,7 @@ fn page_list_sized<S>(
|
||||
ui.set_clip_rect(ui.clip_rect().intersect(rect));
|
||||
footer(ui, state);
|
||||
});
|
||||
scenario::publish_rect("dialog:pages_footer", rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:pages_footer", rect);
|
||||
}
|
||||
let content = egui::Rect::from_min_max(egui::pos2(nav.right(), body.top()), body.max);
|
||||
let padding = egui::epaint::MarginF32 {
|
||||
@@ -1421,14 +1427,14 @@ fn page_list_sized<S>(
|
||||
if let Some(header) = header {
|
||||
header(ui, state);
|
||||
}
|
||||
let force_scroll = search_scroll && crate::prefs_search::force_scroll(ui.ctx());
|
||||
let force_scroll = search_scroll && super::settings_search::force_scroll(ui.ctx());
|
||||
if let Some(page) = pages.get(*selected) {
|
||||
if page.scroll || force_scroll {
|
||||
let scroll = super::settings_scroll(ui, ("sc_prefs_page", *selected), |ui| (page.draw)(ui, state));
|
||||
scenario::publish_rect("dialog:page_body", scroll.inner_rect);
|
||||
solidcraft_ui_registry::publish_rect("dialog:page_body", scroll.inner_rect);
|
||||
} else {
|
||||
(page.draw)(ui, state);
|
||||
scenario::publish_rect("dialog:page_body", content);
|
||||
solidcraft_ui_registry::publish_rect("dialog:page_body", content);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ fn closed_backing_window_noop_does_not_block_real_host_menu_but_visible_returnin
|
||||
observed.0 = ui.ctx().read_response(source.id).unwrap().clicked();
|
||||
let id = egui::Id::unique("returning_backing_feature");
|
||||
let container = Container::new(ui.ctx(), Surface::Feature);
|
||||
let backing = Panel::feature_frame(ui.ctx(), crate::shell::LayoutMode::Floating, crate::frame::Side::Left);
|
||||
let backing = Panel::feature_frame(ui.ctx(), crate::ui_kit::LayoutMode::Floating, crate::frame::Side::Left);
|
||||
let stock = container
|
||||
.window("Returning independent feature")
|
||||
.id(id)
|
||||
@@ -273,7 +273,7 @@ fn actual_feature_and_system_windows_publish_visible_paint_but_popups_and_modals
|
||||
Container::begin_host_windows(ui.ctx());
|
||||
let id = egui::Id::unique("actual_feature_ledger_producer");
|
||||
let container = Container::new(ui.ctx(), Surface::Feature);
|
||||
let frame = Panel::feature_frame(ui.ctx(), crate::shell::LayoutMode::Floating, crate::frame::Side::Left);
|
||||
let frame = Panel::feature_frame(ui.ctx(), crate::ui_kit::LayoutMode::Floating, crate::frame::Side::Left);
|
||||
let stock = container
|
||||
.window("Persistent feature")
|
||||
.id(id)
|
||||
@@ -521,7 +521,7 @@ fn universal_kit_chrome_uses_one_container_and_preserves_parent_clipping_and_foc
|
||||
|
||||
#[test]
|
||||
fn universal_surface_uses_adaptive_workspace_fill_and_rounding_with_safe_content_clip() {
|
||||
use crate::shell::LayoutMode;
|
||||
use crate::ui_kit::LayoutMode;
|
||||
let bounds = egui::Rect::from_min_size(egui::pos2(40.0, 40.0), egui::vec2(320.0, 180.0));
|
||||
for dark in [false, true] {
|
||||
let mut tokens = if dark { Tokens::dark() } else { Tokens::light() };
|
||||
@@ -969,7 +969,7 @@ fn host_marker(painter: &egui::Painter, frame: egui::Frame, content: egui::Rect,
|
||||
fn blur_host_fixture(host: BlurHost, mode: u8, position: egui::Pos2, transform: egui::emath::TSTransform) -> (egui::FullOutput, String) {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(640.0, 480.0));
|
||||
let container = Container::for_layout(crate::shell::LayoutMode::Floating, Surface::Feature);
|
||||
let container = Container::for_layout(crate::ui_kit::LayoutMode::Floating, Surface::Feature);
|
||||
let frame = container
|
||||
.frame(&ctx, egui::Color32::from_rgba_unmultiplied(40, 80, 100, 180), crate::frame::Side::Left)
|
||||
.stroke(egui::Stroke::new(2.5, egui::Color32::WHITE))
|
||||
@@ -1070,7 +1070,7 @@ fn blur_stock_hosts_zero_has_identical_shapes_noops_geometry_and_ids() {
|
||||
fn blur_stock_window_open_flag_preserves_closed_close_and_reopen_behavior() {
|
||||
fn record(mode: u8, omit_markers: bool) -> Vec<(Vec<String>, String)> {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let container = Container::for_layout(crate::shell::LayoutMode::Floating, Surface::Feature);
|
||||
let container = Container::for_layout(crate::ui_kit::LayoutMode::Floating, Surface::Feature);
|
||||
let frame = container.frame(&ctx, Tokens::get().dialog_body, crate::frame::Side::Left);
|
||||
let id = egui::Id::unique("open flag fixture");
|
||||
let mut recorded = Vec::new();
|
||||
@@ -1222,7 +1222,7 @@ fn blur_stock_hosts_late_scaled_layers_and_moved_windows_keep_final_geometry() {
|
||||
fn blur_stock_frame_marker_uses_final_joined_corner_radii() {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
|
||||
let host = Container::for_layout(crate::shell::LayoutMode::Docked, Surface::Feature).joined(DockEdges::default());
|
||||
let host = Container::for_layout(crate::ui_kit::LayoutMode::Docked, Surface::Feature).joined(DockEdges::default());
|
||||
let frame = host.frame(ui.ctx(), Tokens::get().dialog_body, crate::frame::Side::Left);
|
||||
let bounds = ui.available_rect_before_wrap();
|
||||
host.show_frame_background(ui, frame, bounds, Some(host_marker), |ui| {
|
||||
@@ -1336,7 +1336,7 @@ fn heading_weight_field_surfaces_and_disclosures_follow_tokens_in_both_themes()
|
||||
|
||||
#[test]
|
||||
fn panel_keeps_actions_and_body_geometry_in_both_layouts() {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let vp = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(640.0, 480.0));
|
||||
for _ in 0..3 {
|
||||
@@ -1344,7 +1344,7 @@ fn panel_keeps_actions_and_body_geometry_in_both_layouts() {
|
||||
crate::test_support::frame(&ctx, egui::RawInput { screen_rect: Some(vp), ..Default::default() }, |_| {
|
||||
let side = crate::frame::Side::Right;
|
||||
let frame = Panel::feature_frame(&ctx, layout, side);
|
||||
let anchor = egui::pos2(vp.right() - crate::shell::SHELL_GAP, vp.top());
|
||||
let anchor = egui::pos2(vp.right() - crate::ui_kit::SHELL_GAP, vp.top());
|
||||
let panel = Panel::new("Fillet", egui::Id::unique("gallery_feature"), anchor, vp, side, frame);
|
||||
panel.window(&ctx).show(&ctx, |ui| {
|
||||
panel.feature(
|
||||
@@ -1400,8 +1400,8 @@ fn feature_frame_with(
|
||||
crate::test_support::frame(ctx, egui::RawInput { screen_rect: Some(vp), events, focused: true, ..Default::default() }, |_| {
|
||||
placement.publish(ctx);
|
||||
let side = crate::frame::Side::Right;
|
||||
let frame = Panel::feature_frame(ctx, crate::shell::LayoutMode::Floating, side);
|
||||
let anchor = egui::pos2(vp.right() - crate::shell::SHELL_GAP, vp.top());
|
||||
let frame = Panel::feature_frame(ctx, crate::ui_kit::LayoutMode::Floating, side);
|
||||
let anchor = egui::pos2(vp.right() - crate::ui_kit::SHELL_GAP, vp.top());
|
||||
let panel = Panel::new("Fillet", egui::Id::unique("top_actions"), anchor, vp, side, frame);
|
||||
panel.window(ctx).show(ctx, |ui| {
|
||||
let heading = FeatureOptions {
|
||||
@@ -1489,14 +1489,14 @@ fn top_placement_shows_the_compute_status_in_the_title_row() {
|
||||
output = Some(crate::test_support::frame(&ctx, egui::RawInput { screen_rect: Some(vp), ..Default::default() }, |_| {
|
||||
DialogButtons::Top.publish(&ctx);
|
||||
let side = crate::frame::Side::Right;
|
||||
let anchor = egui::pos2(vp.right() - crate::shell::SHELL_GAP, vp.top());
|
||||
let anchor = egui::pos2(vp.right() - crate::ui_kit::SHELL_GAP, vp.top());
|
||||
let panel = Panel::new(
|
||||
"Fillet",
|
||||
egui::Id::unique("top_status"),
|
||||
anchor,
|
||||
vp,
|
||||
side,
|
||||
Panel::feature_frame(&ctx, crate::shell::LayoutMode::Floating, side),
|
||||
Panel::feature_frame(&ctx, crate::ui_kit::LayoutMode::Floating, side),
|
||||
);
|
||||
panel.window(&ctx).show(&ctx, |ui| {
|
||||
let heading = FeatureOptions {
|
||||
@@ -1532,14 +1532,14 @@ fn bottom_placement_puts_the_busy_spinner_in_the_footer_row() {
|
||||
crate::test_support::frame(&ctx, egui::RawInput { screen_rect: Some(vp), ..Default::default() }, |_| {
|
||||
DialogButtons::Bottom.publish(&ctx);
|
||||
let side = crate::frame::Side::Right;
|
||||
let anchor = egui::pos2(vp.right() - crate::shell::SHELL_GAP, vp.top());
|
||||
let anchor = egui::pos2(vp.right() - crate::ui_kit::SHELL_GAP, vp.top());
|
||||
let panel = Panel::new(
|
||||
"Fillet",
|
||||
egui::Id::unique("bottom_status"),
|
||||
anchor,
|
||||
vp,
|
||||
side,
|
||||
Panel::feature_frame(&ctx, crate::shell::LayoutMode::Floating, side),
|
||||
Panel::feature_frame(&ctx, crate::ui_kit::LayoutMode::Floating, side),
|
||||
);
|
||||
panel.window(&ctx).show(&ctx, |ui| {
|
||||
let heading = FeatureOptions {
|
||||
@@ -1585,7 +1585,7 @@ fn top_placement_keeps_a_managers_fold_beside_the_actions() {
|
||||
|
||||
#[test]
|
||||
fn palette_scrolls_short_views_and_retains_its_status_and_action_handles() {
|
||||
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
|
||||
for height in [125.0, 480.0] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let vp = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(640.0, height));
|
||||
@@ -1593,11 +1593,11 @@ fn palette_scrolls_short_views_and_retains_its_status_and_action_handles() {
|
||||
scenario::clear_handles();
|
||||
crate::test_support::frame(&ctx, egui::RawInput { screen_rect: Some(vp), ..Default::default() }, |_| {
|
||||
let frame = Panel::palette_frame(&ctx, layout);
|
||||
let bottom = vp.bottom() - crate::shell::SHELL_GAP;
|
||||
let bottom = vp.bottom() - crate::ui_kit::SHELL_GAP;
|
||||
let panel = Panel::new(
|
||||
"Options",
|
||||
egui::Id::unique("gallery_palette"),
|
||||
egui::pos2(vp.right() - crate::shell::SHELL_GAP, 0.0),
|
||||
egui::pos2(vp.right() - crate::ui_kit::SHELL_GAP, 0.0),
|
||||
vp,
|
||||
crate::frame::Side::Right,
|
||||
frame,
|
||||
@@ -1642,7 +1642,7 @@ fn palette_scrolls_short_views_and_retains_its_status_and_action_handles() {
|
||||
|
||||
#[test]
|
||||
fn wide_tables_remain_bounded_with_pinned_actions_on_small_viewports() {
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for width in [240.0, 640.0, 1200.0] {
|
||||
let ctx = crate::fonts::test_context();
|
||||
let viewport = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, 360.0));
|
||||
@@ -2187,7 +2187,7 @@ fn window_footer_is_optional_and_can_contain_only_a_left_action() {
|
||||
|
||||
#[test]
|
||||
fn universal_containers_follow_one_layout_without_changing_transient_surfaces() {
|
||||
use crate::{frame::Side, shell::LayoutMode};
|
||||
use crate::{frame::Side, ui_kit::LayoutMode};
|
||||
let ctx = crate::fonts::test_context();
|
||||
let previous = crate::theme::is_dark();
|
||||
for dark in [false, true] {
|
||||
@@ -2302,7 +2302,7 @@ fn window_body_buttons_stay_visible_after_the_page_grows_from_nothing() {
|
||||
#[test]
|
||||
fn adjoining_compact_frames_share_one_stroke_and_only_exposed_corners_round() {
|
||||
use crate::ui_kit::DockEdges;
|
||||
use crate::{frame::Side, shell::LayoutMode};
|
||||
use crate::{frame::Side, ui_kit::LayoutMode};
|
||||
let ctx = crate::fonts::test_context();
|
||||
LayoutMode::Docked.publish(&ctx);
|
||||
let t = Tokens::get();
|
||||
@@ -2324,7 +2324,7 @@ fn adjoining_compact_frames_share_one_stroke_and_only_exposed_corners_round() {
|
||||
#[test]
|
||||
fn compact_tool_sheet_corners_follow_its_right_edge_contact() {
|
||||
let ctx = crate::fonts::test_context();
|
||||
crate::shell::LayoutMode::Docked.publish(&ctx);
|
||||
crate::ui_kit::LayoutMode::Docked.publish(&ctx);
|
||||
let t = Tokens::get();
|
||||
let container = Container::new(&ctx, Surface::Feature);
|
||||
let frame = container.frame(&ctx, t.dialog_body, crate::frame::Side::Right);
|
||||
@@ -2334,7 +2334,7 @@ fn compact_tool_sheet_corners_follow_its_right_edge_contact() {
|
||||
|
||||
#[test]
|
||||
fn theme_shape_changes_preserve_panel_geometry_and_dock_contacts() {
|
||||
use crate::{frame::Side, shell::LayoutMode};
|
||||
use crate::{frame::Side, ui_kit::LayoutMode};
|
||||
let ctx = crate::fonts::test_context();
|
||||
let mut results = Vec::new();
|
||||
for name in ["solidcraft-95", "blueprint", "cupertino"] {
|
||||
@@ -2382,7 +2382,7 @@ fn border_hierarchy_reaches_hosts_sections_controls_and_inset_without_new_target
|
||||
let ctx = crate::fonts::test_context();
|
||||
let mut value = "12 mm".to_string();
|
||||
let output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
|
||||
let container = Container::for_layout(crate::shell::LayoutMode::Floating, Surface::Panel);
|
||||
let container = Container::for_layout(crate::ui_kit::LayoutMode::Floating, Surface::Panel);
|
||||
let frame = container.frame(ui.ctx(), t.panel, crate::frame::Side::Left).inner_margin(12);
|
||||
let bounds = ui.max_rect();
|
||||
container.show_frame(ui, frame, bounds, |ui| {
|
||||
@@ -2421,7 +2421,7 @@ fn border_hierarchy_reaches_hosts_sections_controls_and_inset_without_new_target
|
||||
});
|
||||
}
|
||||
assert!(found.into_iter().all(|v| v), "missing role: {found:?}");
|
||||
for layout in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
|
||||
for layout in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
|
||||
for surface in [
|
||||
Surface::Panel,
|
||||
Surface::Chrome,
|
||||
@@ -2448,7 +2448,7 @@ fn inset_border_skips_tiny_or_invalid_surfaces_and_keeps_dock_contacts_square()
|
||||
let _restore = crate::theme::scoped_tokens(t);
|
||||
let ctx = crate::fonts::test_context();
|
||||
let output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
|
||||
let frame = Container::for_layout(crate::shell::LayoutMode::Docked, Surface::Panel).joined(DockEdges::ALL).frame(
|
||||
let frame = Container::for_layout(crate::ui_kit::LayoutMode::Docked, Surface::Panel).joined(DockEdges::ALL).frame(
|
||||
&ctx,
|
||||
t.panel,
|
||||
crate::frame::Side::Left,
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
//! never additional application viewports; every colour and control metric comes from tokens.
|
||||
pub(crate) use super::preferences_figure_data::{paint_assembly_move, paint_direct_interaction, paint_measure_axis_distances, paint_tool_cursor};
|
||||
use super::{metrics::dimension, text::paint_text};
|
||||
use crate::theme::{ColorVision, Tokens};
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::{
|
||||
scenario,
|
||||
theme::{ColorVision, Tokens},
|
||||
};
|
||||
use egui::{Color32, FontId, Pos2, Rect, Response, Sense, Stroke, Ui, pos2, vec2};
|
||||
|
||||
pub(super) const PAGE_SCROLL_GUTTER: &str = "sc_page_scroll_gutter";
|
||||
@@ -35,7 +32,7 @@ fn blur_cost_meter(ui: &mut Ui, radius: f32) {
|
||||
ui.painter().rect_filled(bar, t.radius_sm, if i < filled { t.form_primary } else { t.dialog_rule });
|
||||
}
|
||||
}
|
||||
scenario::publish_rect("prefs:blur_cost", rect);
|
||||
solidcraft_ui_registry::publish_rect("prefs:blur_cost", rect);
|
||||
response.backing_on_hover_text(crate::i18n::tr("prefs.blur_cost"));
|
||||
}
|
||||
|
||||
@@ -52,11 +49,11 @@ pub(crate) fn theme_option_notice(ui: &mut Ui, id: &str, text: &str, can_undo: b
|
||||
let mut draw_actions = |ui: &mut Ui| {
|
||||
if can_undo {
|
||||
let response = super::text_button(ui, crate::i18n::tr("ui.undo"), true);
|
||||
scenario::publish_target("prefs:theme_notice_undo:guard", response.rect);
|
||||
solidcraft_ui_registry::publish_target("prefs:theme_notice_undo:guard", response.rect);
|
||||
undo = response.clicked();
|
||||
}
|
||||
let response = super::text_button(ui, dismiss_label, true);
|
||||
scenario::publish_target(&format!("prefs:theme_notice_dismiss:{id}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("prefs:theme_notice_dismiss:{id}"), response.rect);
|
||||
dismiss = response.clicked();
|
||||
};
|
||||
let width = dimension(ui.available_width());
|
||||
@@ -80,7 +77,7 @@ pub(crate) fn theme_option_notice(ui: &mut Ui, id: &str, text: &str, can_undo: b
|
||||
});
|
||||
}
|
||||
});
|
||||
scenario::publish_rect(&format!("prefs:theme_notice:{id}"), response.response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:theme_notice:{id}"), response.response.rect);
|
||||
(undo, dismiss)
|
||||
}
|
||||
|
||||
@@ -218,11 +215,11 @@ pub(crate) fn theme_options_list(
|
||||
);
|
||||
if options.iter().any(|option| values.get(&option.definition.id).is_some_and(|v| *v != option.definition.default_value())) {
|
||||
let response = super::text_button(ui, crate::i18n::tr("prefs.theme_options_reset"), true);
|
||||
scenario::publish_target("prefs:theme_options_reset", response.rect);
|
||||
solidcraft_ui_registry::publish_target("prefs:theme_options_reset", response.rect);
|
||||
reset = response.clicked();
|
||||
}
|
||||
});
|
||||
scenario::publish_rect("prefs:theme_options_header", header.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("prefs:theme_options_header", header.response.rect);
|
||||
let mut draw_rows = |ui: &mut Ui, rows: &[Vec<usize>]| {
|
||||
let mut section = None;
|
||||
for row in rows {
|
||||
@@ -266,7 +263,10 @@ pub(crate) fn theme_options_list(
|
||||
}
|
||||
});
|
||||
if let Some(option) = cell.first().and_then(|i| options.get(*i)) {
|
||||
scenario::publish_rect(&format!("prefs:theme_option_cell:{}", option.definition.id), response.response.rect);
|
||||
solidcraft_ui_registry::publish_rect(
|
||||
&format!("prefs:theme_option_cell:{}", option.definition.id),
|
||||
response.response.rect,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -278,8 +278,8 @@ pub(crate) fn theme_options_list(
|
||||
if plan.more_count > 0 {
|
||||
let title = crate::i18n::format("prefs.theme_options_more", &[("count", &plan.more_count.to_string())]);
|
||||
super::section(ui, "more", &title, false, |ui| draw_rows(ui, &plan.more));
|
||||
if let Some(rect) = scenario::rect_of(&format!("dialog:section:{title}")) {
|
||||
scenario::publish_target("prefs:theme_options_more", rect);
|
||||
if let Some(rect) = solidcraft_ui_registry::rect_of(&format!("dialog:section:{title}")) {
|
||||
solidcraft_ui_registry::publish_target("prefs:theme_options_more", rect);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -351,7 +351,7 @@ fn theme_option_control(
|
||||
super::text(ui, &label, super::Text::Inline);
|
||||
},
|
||||
);
|
||||
scenario::publish_rect(&format!("prefs:theme_option_readout:{}", def.id), readout.response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:theme_option_readout:{}", def.id), readout.response.rect);
|
||||
if option.blur {
|
||||
blur_cost_meter(ui, *n);
|
||||
}
|
||||
@@ -378,10 +378,10 @@ fn theme_option_control(
|
||||
.response
|
||||
});
|
||||
let response = response.inner.backing_on_hover_text(&option.help);
|
||||
scenario::publish_target(&format!("prefs:theme_option:{}", def.id), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("prefs:theme_option:{}", def.id), response.rect);
|
||||
if let Some(reason) = &option.reason {
|
||||
let note = ui.horizontal(|ui| super::settings_inline_note(ui, reason));
|
||||
scenario::publish_rect(&format!("prefs:theme_option_reason:{}", def.id), note.response.rect);
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:theme_option_reason:{}", def.id), note.response.rect);
|
||||
}
|
||||
if active && value != before {
|
||||
values.insert(def.id.clone(), value);
|
||||
@@ -402,7 +402,7 @@ fn theme_option_switch(ui: &mut Ui, label: &str, on: &mut bool) {
|
||||
ui.painter().rect_stroke(rect, t.control_radius, Stroke::new(t.space_2, t.form_primary), egui::StrokeKind::Inside);
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::CheckBox, ui.is_enabled(), *on, label));
|
||||
scenario::publish_target(&format!("prefs:theme_option_switch:{label}"), response.rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("prefs:theme_option_switch:{label}"), response.rect);
|
||||
}
|
||||
|
||||
/// Page scroller keeps content aligned while centring its bar in the window's right inset.
|
||||
@@ -451,7 +451,7 @@ pub(crate) fn settings_heading(ui: &mut Ui, title: &str, description: &str) {
|
||||
}
|
||||
|
||||
pub(crate) fn settings_heading_with_tag(ui: &mut Ui, title: &str, description: &str, tag: Option<fn(&mut Ui)>) {
|
||||
if crate::prefs_search::querying(ui) {
|
||||
if super::settings_search::querying(ui) {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -478,7 +478,7 @@ pub(crate) fn settings_heading_with_tag(ui: &mut Ui, title: &str, description: &
|
||||
super::text(ui, description, super::Text::Hint("settings_heading"));
|
||||
});
|
||||
if page {
|
||||
scenario::publish_rect("prefs:page_heading", heading.response.rect);
|
||||
solidcraft_ui_registry::publish_rect("prefs:page_heading", heading.response.rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,33 +505,33 @@ fn settings_section_count_spaced<R>(
|
||||
compact: bool,
|
||||
mut add: impl FnMut(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
if crate::prefs_search::querying(ui) {
|
||||
crate::prefs_search::begin_section(ui, title);
|
||||
if super::settings_search::querying(ui) {
|
||||
super::settings_search::begin_section(ui, title);
|
||||
if !ui.is_visible() {
|
||||
return Some(add(ui));
|
||||
}
|
||||
if crate::prefs_search::shows_all(ui) {
|
||||
let mut child = crate::prefs_search::detached_child(ui, ("prefs-search-section", key), false);
|
||||
if super::settings_search::shows_all(ui) {
|
||||
let mut child = super::settings_search::detached_child(ui, ("prefs-search-section", key), false);
|
||||
let result = add(&mut child);
|
||||
crate::prefs_search::place_search_child(ui, &child);
|
||||
super::settings_search::place_search_child(ui, &child);
|
||||
return Some(result);
|
||||
}
|
||||
let mark = crate::prefs_search::search_layout_mark(ui);
|
||||
let before = crate::prefs_search::hits(ui);
|
||||
let mark = super::settings_search::search_layout_mark(ui);
|
||||
let before = super::settings_search::hits(ui);
|
||||
let (gained, missed) = {
|
||||
let mut probe = crate::prefs_search::detached_child(ui, ("prefs-search-section-probe", key), true);
|
||||
let mut probe = super::settings_search::detached_child(ui, ("prefs-search-section-probe", key), true);
|
||||
let missed = add(&mut probe);
|
||||
(crate::prefs_search::hits(ui) > before, missed)
|
||||
(super::settings_search::hits(ui) > before, missed)
|
||||
};
|
||||
crate::prefs_search::restore_search_layout(ui, mark);
|
||||
super::settings_search::restore_search_layout(ui, mark);
|
||||
if !gained {
|
||||
return Some(missed);
|
||||
}
|
||||
// Keep search headings outside wrapped rows in this section.
|
||||
crate::prefs_search::reveal(ui);
|
||||
let mut child = crate::prefs_search::detached_child(ui, ("prefs-search-section", key), false);
|
||||
super::settings_search::reveal(ui);
|
||||
let mut child = super::settings_search::detached_child(ui, ("prefs-search-section", key), false);
|
||||
let result = add(&mut child);
|
||||
crate::prefs_search::place_search_child(ui, &child);
|
||||
super::settings_search::place_search_child(ui, &child);
|
||||
return Some(result);
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -547,14 +547,14 @@ fn settings_section_count_spaced<R>(
|
||||
ui.spacing_mut().item_spacing.y = t.space_10;
|
||||
}
|
||||
let result = super::section_count(ui, key, title, true, count, |ui| {
|
||||
if let Some(rect) = scenario::rect_of(&format!("dialog:section:{title}")) {
|
||||
scenario::publish_rect(&format!("prefs:section:{key}"), rect);
|
||||
if let Some(rect) = solidcraft_ui_registry::rect_of(&format!("dialog:section:{title}")) {
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:section:{key}"), rect);
|
||||
}
|
||||
ui.spacing_mut().item_spacing.y = if page { t.space_14 } else { t.space_8 };
|
||||
let start = ui.next_widget_position();
|
||||
let result = add(ui);
|
||||
if page {
|
||||
scenario::publish_rect(&format!("prefs:section:{key}:body"), Rect::from_min_max(start, ui.min_rect().max));
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:section:{key}:body"), Rect::from_min_max(start, ui.min_rect().max));
|
||||
}
|
||||
result
|
||||
});
|
||||
@@ -580,7 +580,7 @@ pub(super) fn settings_card_frame(ui: &Ui) -> egui::Frame {
|
||||
|
||||
/// Controls and one explanatory specimen side by side, stacked on narrow viewports.
|
||||
pub(crate) fn settings_pair<R>(ui: &mut Ui, controls: impl FnOnce(&mut Ui) -> R, preview: impl FnOnce(&mut Ui, R)) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
let _ = controls(ui);
|
||||
return;
|
||||
}
|
||||
@@ -607,7 +607,7 @@ pub(crate) fn settings_pair<R>(ui: &mut Ui, controls: impl FnOnce(&mut Ui) -> R,
|
||||
|
||||
/// Compact informational state badge for settings; never an interactive control.
|
||||
pub(crate) fn settings_badge(ui: &mut Ui, text: &str, warning: bool) {
|
||||
if crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[text]) {
|
||||
if super::settings_search::suppress_chrome(ui) && !super::settings_search::texts_hit(ui, &[text]) {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -642,13 +642,13 @@ pub(crate) enum PreferenceFigure {
|
||||
/// Reusable illustrated radio card. Width comes from the figure, and wrapping is owned by
|
||||
/// the parent; labels remain accessible even when translations need an ellipsis.
|
||||
pub(crate) fn option_card(ui: &mut Ui, label: &str, note: Option<&str>, selected: bool, enabled: bool, figure: PreferenceFigure) -> Response {
|
||||
if crate::prefs_search::querying(ui) {
|
||||
if super::settings_search::querying(ui) {
|
||||
let show = match note {
|
||||
Some(note) => crate::prefs_search::visible(ui, &[label, note]),
|
||||
None => crate::prefs_search::visible(ui, &[label]),
|
||||
Some(note) => super::settings_search::visible(ui, &[label, note]),
|
||||
None => super::settings_search::visible(ui, &[label]),
|
||||
};
|
||||
if !show {
|
||||
return crate::prefs_search::hidden_response(ui);
|
||||
return super::settings_search::hidden_response(ui);
|
||||
}
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -725,7 +725,7 @@ pub(crate) fn option_card(ui: &mut Ui, label: &str, note: Option<&str>, selected
|
||||
ui.painter().line_segment([centre + vec2(-t.space_2, t.space_2), centre + vec2(t.space_4, -t.space_3)], Stroke::new(2.0, ink));
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::RadioButton, enabled, selected, label));
|
||||
scenario::publish_target(&format!("prefs:option:{label}"), rect);
|
||||
solidcraft_ui_registry::publish_target(&format!("prefs:option:{label}"), rect);
|
||||
let response = response.backing_on_hover_text(label);
|
||||
if enabled { response.on_hover_cursor(egui::CursorIcon::PointingHand) } else { response }
|
||||
}
|
||||
@@ -925,7 +925,7 @@ pub(crate) fn settings_font_row(ui: &mut Ui, label: &str, mut add: impl FnMut(&m
|
||||
|
||||
/// Natural-width dim text, for inline suffixes and metadata without a full-width note row.
|
||||
pub(crate) fn settings_inline_note(ui: &mut Ui, text: &str) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -941,7 +941,7 @@ pub(crate) fn settings_inline_note(ui: &mut Ui, text: &str) {
|
||||
|
||||
/// Framed font specimen, inheriting the chosen application font and its fallback glyphs.
|
||||
pub(crate) fn font_specimen(ui: &mut Ui, text: &str) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -952,7 +952,10 @@ pub(crate) fn font_specimen(ui: &mut Ui, text: &str) {
|
||||
|
||||
/// Intrinsically sized segments, retaining the usual row label for accessibility and automation.
|
||||
pub(crate) fn settings_segments(ui: &mut Ui, label: &str, selected: &mut usize, options: &[&str]) -> bool {
|
||||
if crate::prefs_search::suppress_chrome(ui) && !crate::prefs_search::texts_hit(ui, &[label]) && !crate::prefs_search::texts_hit(ui, options) {
|
||||
if super::settings_search::suppress_chrome(ui)
|
||||
&& !super::settings_search::texts_hit(ui, &[label])
|
||||
&& !super::settings_search::texts_hit(ui, options)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -969,7 +972,7 @@ pub(crate) fn settings_segments(ui: &mut Ui, label: &str, selected: &mut usize,
|
||||
}
|
||||
|
||||
pub(crate) fn status_palette_sample(ui: &mut Ui, mode: ColorVision) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
@@ -985,7 +988,7 @@ pub(crate) fn status_palette_sample(ui: &mut Ui, mode: ColorVision) {
|
||||
let label = crate::i18n::tr(label);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_owned(), crate::fonts::semibold(t.font_small), ink);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(galley.size().x + t.space_24 + t.space_16, t.space_24), Sense::hover());
|
||||
scenario::publish_rect(&format!("prefs:status_sample:{label}"), r);
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:status_sample:{label}"), r);
|
||||
ui.painter().rect_filled(r, t.radius_xl, fill);
|
||||
crate::icons::paint_ink(
|
||||
ui.painter(),
|
||||
@@ -1007,7 +1010,7 @@ pub(crate) const COLOUR_VISION_SAMPLE: [&str; 6] =
|
||||
/// colours of `t` (the preference passes the mode being chosen, #7152). The six cells share one
|
||||
/// responsive, clipped area.
|
||||
pub(crate) fn colour_vision_sample(ui: &mut egui::Ui, t: &Tokens) {
|
||||
if crate::prefs_search::suppress_chrome(ui) {
|
||||
if super::settings_search::suppress_chrome(ui) {
|
||||
return;
|
||||
}
|
||||
let count = COLOUR_VISION_SAMPLE.len() as f32;
|
||||
@@ -1015,11 +1018,11 @@ pub(crate) fn colour_vision_sample(ui: &mut egui::Ui, t: &Tokens) {
|
||||
let width = ((dimension(ui.available_width()) - gap * (count - 1.0)) / count).min(t.space_24 * 3.0 + t.space_12);
|
||||
let figure_height = t.space_24 + t.space_16 + t.space_4;
|
||||
let (area, _) = ui.allocate_exact_size(vec2(width * count + gap * (count - 1.0), figure_height + t.space_3 + t.space_16), Sense::hover());
|
||||
scenario::publish_rect("prefs:colour_vision_samples", area);
|
||||
solidcraft_ui_registry::publish_rect("prefs:colour_vision_samples", area);
|
||||
let p = ui.painter_at(area);
|
||||
for (i, name) in COLOUR_VISION_SAMPLE.iter().enumerate() {
|
||||
let cell = egui::Rect::from_min_size(area.min + vec2((width + gap) * i as f32, 0.0), vec2(width, figure_height));
|
||||
scenario::publish_rect(&format!("prefs:colour_vision_sample:{i}"), cell);
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:colour_vision_sample:{i}"), cell);
|
||||
super::preferences_figure_ops::paint_colour_vision(&p, cell, i, t);
|
||||
centred_text(
|
||||
&p,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user