Compare commits

...

8 Commits

Author SHA1 Message Date
jyviko e3caced67b UI kit: navigation-bar values and the horizontal wheel in the kit (#13259)
The bar's edge, size, idle and button-kind values move beside the bar's drawing in
ui_kit::navigation as NavigationEdge, NavigationSize, NavigationIdle and NavigationKind (the
kit already has a container Edge). Serde names are unchanged, so saved preferences load as
before. take_horizontal_wheel moves into the theme picker, its only caller. SolidCraft's
slot table stays in navigation_bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 23:46:44 -05:00
jyviko d996441241 UI kit: number parsing and recent colours in the kit, expression support passed in by the host (#13258)
parse_number (decimal point or comma, Unicode minus) moves from prefs into the kit's
fields, and recent_colors moves whole into ui_kit::recent_colors beside the colour picker.
The kit no longer knows the expression language: a host installs an Expressions (is this
a literal, is this a unit, complete or step this text) per egui context, and fields read
it for label drags and completion. SolidCraft installs DocExpressions, built on doc::expr
and params_dialog::complete, when the app first draws into a context. Behaviour in the app
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 23:46:44 -05:00
jyviko 8a40998c2a UI kit: the kit owns the menu entry and the list width (#13257)
Item (a command or ui.* action with its label, shortcut, icon, params, children, swatch
and tip) and LIST_W move from context_menu into ui_kit::menu, with the same serde form, so
the control channel's menu report is byte-identical. context_menu keeps building
SolidCraft's entries (cmd, and act over Item::action) and returns kit Items. No kit file
refers to context_menu any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 23:46:44 -05:00
jyviko 924585be0a UI kit: Preferences search is a kit module, settings_search (#13256)
prefs_search moves whole into ui_kit::settings_search: it filters any settings page the
kit draws, and SolidCraft's Preferences window is one use of it. It publishes its handles
into the widget registry and reads the same wall clock as before (web_time), so no kit
file calls the app for search any more. Every caller uses the new path; state ids, handle
names, matches and scores are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 23:26:23 -05:00
jyviko b2c0fd71d0 Shared UI crates, tranche 1: merge #12936 and #12938 for the batch 2026-11-07 22:46:18 -05:00
jyviko 2605261665 Theme: the tab accent takes the tab, the sketch and the tool as data (#12938)
TabAccent::publish takes the open tab, whether a sketch is being edited and whether a tool
or dialog is active, instead of SolveApp; SolveApp::publish_tab_accent works the three out
in one place. scoped_panel_tokens is public, so a host can draw any component in another
palette or accent. The theme and its tests no longer name SolveApp, Services or workspace.
Accents are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 22:46:17 -05:00
jyviko 397bbf38c7 UI kit: the widget registry is a shared crate, ui-registry (#12936)
The handle, rect and count registry that UI tests, scenarios and the control channel read
moves out of scenario into solidcraft-ui-registry, the first Shared crate. The kit publishes
into it directly; scenario re-exports it, so scenarios, scripts and the rest of ui-egui are
unchanged and every handle keeps its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 22:46:16 -05:00
jyviko 4db733ec68 Layers: a Shared crate class, and cargo xtask shared builds each one on its own (#12935)
A Shared crate depends only on other Shared crates, solidcore and external crates (UI
toolkits included); any layered or testkit crate may use one. The layer check reports a
Shared crate's dependency on an app crate, of any kind. cargo xtask shared checks each
Shared crate's manifest (workspace lints, licence, README) and that no include_str! or
include_bytes! reaches outside it, then builds and tests it with its Shared dependencies in
a scratch workspace of its own. cargo xtask ci runs it after the layer check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-07 22:46:16 -05:00
111 changed files with 1933 additions and 1181 deletions
Generated
+8
View File
@@ -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"
+1
View File
@@ -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" }
+2
View File
@@ -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 }
+1 -1
View File
@@ -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());
+26 -26
View File
@@ -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},
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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;
+2 -2
View File
@@ -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| {
@@ -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| {
+1 -5
View File
@@ -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!(
+2 -97
View File
@@ -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
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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");
}
}
+2 -1
View File
@@ -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;
+12 -12
View File
@@ -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
}
+12 -6
View File
@@ -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;
@@ -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());
@@ -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);
@@ -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);
+1 -1
View File
@@ -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")
}
+1 -1
View File
@@ -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) {
+3 -2
View File
@@ -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);
+2 -1
View File
@@ -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;
+2 -17
View File
@@ -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();
+17 -55
View File
@@ -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 {
+22 -20
View File
@@ -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 });
}
}
}
+24 -24
View File
@@ -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);
+1 -1
View File
@@ -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) {
+26
View File
@@ -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::*;
+15 -15
View File
@@ -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);
+57 -64
View File
@@ -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.
@@ -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>,
}
@@ -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");
}
@@ -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}");
}
}
+3 -3
View File
@@ -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"));
}
});
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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(
+4 -4
View File
@@ -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);
}
}
}
+22 -17
View File
@@ -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,
+4 -4
View File
@@ -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);
}
}
}
+7 -7
View File
@@ -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);
})
})
});
+21 -21
View File
@@ -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),
+2 -2
View File
@@ -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();
+17 -17
View File
@@ -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);
}
}
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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;
@@ -519,7 +519,7 @@ fn stage_catalogs_fill_a_290_by_530_panel_and_keep_two_line_labels_inside_tiles(
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.
+7 -66
View File
@@ -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(());
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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 -1
View File
@@ -2530,7 +2530,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.
+2 -2
View File
@@ -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
{
+28 -29
View File
@@ -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");
}
}
+5 -5
View File
@@ -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()
}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+22 -22
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+5 -5
View File
@@ -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();
+31 -31
View File
@@ -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
}
+8 -8
View File
@@ -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(|| {
+5 -4
View File
@@ -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]));
+4 -4
View File
@@ -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);
}
+4 -4
View File
@@ -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)]
@@ -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 -2
View File
@@ -583,7 +583,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 +607,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),
+12 -12
View File
@@ -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
+109 -69
View File
@@ -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));
+3 -2
View File
@@ -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);
+2 -2
View File
@@ -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);
}
+68 -68
View File
@@ -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" })))
+4 -4
View File
@@ -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(),
+65 -65
View File
@@ -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))
+1 -1
View File
@@ -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())],
+4 -4
View File
@@ -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},
+102 -5
View File
@@ -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(
+6 -1
View File
@@ -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};
@@ -76,17 +79,19 @@ 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::{
+3 -3
View File
@@ -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 }
}
+66 -21
View File
@@ -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)
+5 -5
View File
@@ -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);
+33 -33
View File
@@ -1,7 +1,7 @@
//! 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;
@@ -64,7 +64,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 +83,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 +103,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 +166,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 +179,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 +192,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 +243,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 +272,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 +376,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 +523,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 +575,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() {
@@ -792,7 +792,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 +816,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 +839,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 +864,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 +915,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 +928,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 +1254,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 +1281,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 +1330,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 +1389,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 +1400,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 +1421,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);
}
}
},
+53 -50
View File
@@ -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,
@@ -1,7 +1,7 @@
//! Shared concise choice cards and the agent-pointer settings specimen.
use super::{metrics::dimension, text::paint_text};
use crate::theme::Tokens;
use crate::ui_kit::BackingTooltipExt as _;
use crate::{scenario, theme::Tokens};
use egui::{FontId, Rect, Response, Sense, Stroke, Ui, pos2, vec2};
/// One concise choice with its full meaning available to keyboard users and on hover.
@@ -15,17 +15,17 @@ pub(crate) struct SettingsChoice<'a> {
/// Equal option cards keep the choices comparable without repeating long explanations.
pub(crate) fn settings_choices(ui: &mut Ui, choices: &[SettingsChoice<'_>], selected: &mut usize) -> bool {
if crate::prefs_search::querying(ui) && !crate::prefs_search::shows_all(ui) {
if super::settings_search::querying(ui) && !super::settings_search::shows_all(ui) {
let keep: Vec<usize> = choices
.iter()
.enumerate()
.filter(|(_, choice)| crate::prefs_search::texts_hit(ui, &[choice.title, choice.summary]))
.filter(|(_, choice)| super::settings_search::texts_hit(ui, &[choice.title, choice.summary]))
.map(|(index, _)| index)
.collect();
if keep.is_empty() {
return false;
}
crate::prefs_search::reveal(ui);
super::settings_search::reveal(ui);
return choice_row(ui, choices, selected, &keep);
}
let keep: Vec<usize> = (0..choices.len()).collect();
@@ -119,7 +119,7 @@ fn choice_card(ui: &mut Ui, width: f32, choice: &SettingsChoice<'_>, selected: b
if let Some(icon) = choice.icon {
let cue = Rect::from_min_size(at, vec2(t.space_14, t.space_14));
crate::icons::paint_icon(ui.painter(), cue, icon, 1.0);
scenario::publish_rect(&format!("{}:icon", choice.key), cue);
solidcraft_ui_registry::publish_rect(&format!("{}:icon", choice.key), cue);
at.x += icon_width;
}
let summary_at = pos2(rect.left() + t.space_12, at.y + title_galley.size().y + t.space_6);
@@ -127,16 +127,16 @@ fn choice_card(ui: &mut Ui, width: f32, choice: &SettingsChoice<'_>, selected: b
ui.painter().galley(summary_at, summary_galley, t.form_dim);
}
let texts = [choice.title, choice.summary];
if let Some(text) = texts.iter().find(|text| crate::prefs_search::texts_hit(ui, &[*text])) {
crate::prefs_search::remember(ui, text, rect);
if let Some(text) = texts.iter().find(|text| super::settings_search::texts_hit(ui, &[*text])) {
super::settings_search::remember(ui, text, rect);
}
for text in texts {
crate::prefs_search::follow(ui, text, rect);
super::settings_search::follow(ui, text, rect);
}
response.widget_info(|| {
egui::WidgetInfo::selected(egui::Role::RadioButton, ui.is_enabled(), selected, format!("{}: {}", choice.title, choice.detail))
});
scenario::publish_target(choice.key, rect);
solidcraft_ui_registry::publish_target(choice.key, rect);
response.backing_on_hover_text(choice.detail).on_hover_cursor(egui::CursorIcon::PointingHand)
}
+25 -25
View File
@@ -3,8 +3,8 @@
//! every colour comes from `Tokens`. A card sizes to the width its container gives it and its
//! figure keeps the figure's aspect ratio.
use super::{metrics::dimension, preferences::centred_text};
use crate::theme::Tokens;
use crate::ui_kit::BackingTooltipExt as _;
use crate::{scenario, theme::Tokens};
use egui::{Color32, FontId, Painter, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
/// How a card or tile looks in a state. Kept separate from painting so the state rules can be tested.
@@ -111,9 +111,9 @@ pub(crate) fn settings_row_card<R>(ui: &mut Ui, selected: bool, height: f32, add
}
fn paint_card(ui: &mut Ui, card: &Card, width: f32, tile: bool) -> Response {
crate::prefs_search::note_unlabelled(ui, &[card.label]);
if crate::prefs_search::querying(ui) && !crate::prefs_search::visible(ui, &[card.label, card.sub.unwrap_or("")]) {
return crate::prefs_search::hidden_response(ui);
super::settings_search::note_unlabelled(ui, &[card.label]);
if super::settings_search::querying(ui) && !super::settings_search::visible(ui, &[card.label, card.sub.unwrap_or("")]) {
return super::settings_search::hidden_response(ui);
}
let t = Tokens::get();
let width = dimension(width);
@@ -122,8 +122,8 @@ fn paint_card(ui: &mut Ui, card: &Card, width: f32, tile: bool) -> Response {
// Labelled compound rows own their search index (settings_picture_choices, for example).
// Standalone cards and tiles are independently searchable settings.
if super::layout::label(ui).is_empty() {
crate::prefs_search::remember(ui, card.label, rect);
crate::prefs_search::follow(ui, card.label, rect);
super::settings_search::remember(ui, card.label, rect);
super::settings_search::follow(ui, card.label, rect);
}
let look = card_look(card.selected, enabled, response.hovered(), response.has_focus());
let p = ui.painter();
@@ -187,7 +187,7 @@ fn paint_card(ui: &mut Ui, card: &Card, width: f32, tile: bool) -> Response {
}
let kind = if tile { egui::Role::CheckBox } else { egui::Role::RadioButton };
response.widget_info(|| egui::WidgetInfo::selected(kind, enabled, card.selected, card.label));
scenario::publish_target(&format!("prefs:card:{}", card.label), rect);
solidcraft_ui_registry::publish_target(&format!("prefs:card:{}", card.label), rect);
let response = response.backing_on_hover_text(card.label);
let response = if let Some(options) = card.options { response.backing_on_hover_text(options) } else { response };
if enabled { response.on_hover_cursor(egui::CursorIcon::PointingHand) } else { response }
@@ -204,7 +204,7 @@ fn tick(p: &Painter, r: Rect, ink: Color32) {
/// A radio card: a figure, a label and an optional sub-label. Click it to choose it.
pub(crate) fn picture_card(ui: &mut Ui, card: &Card, width: f32) -> Response {
crate::prefs_search::note_unlabelled(ui, &[card.sub.unwrap_or("")]);
super::settings_search::note_unlabelled(ui, &[card.sub.unwrap_or("")]);
paint_card(ui, card, width, false)
}
@@ -271,7 +271,7 @@ pub(crate) fn settings_picture_choices_sized(
+ if size.caption { t.space_16 + t.space_10 } else { 0.0 };
super::settings_form(ui, 0.0, |ui| {
super::layout::sized_row(ui, label, height, |ui| {
crate::prefs_search::note_choices(ui, &options.iter().map(|o| o.label).collect::<Vec<_>>());
super::settings_search::note_choices(ui, &options.iter().map(|o| o.label).collect::<Vec<_>>());
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(t.space_10, t.space_10);
// Use the real control child, including the scrollbar and form bounds. Round
@@ -283,19 +283,19 @@ pub(crate) fn settings_picture_choices_sized(
super::settings_inline_note(ui, label);
}
let mut picked = None;
let mut indexed = crate::prefs_search::texts_hit(ui, &[label]);
let mut indexed = super::settings_search::texts_hit(ui, &[label]);
for (row, choices) in options.chunks(columns).enumerate() {
ui.horizontal_top(|ui| {
for (column, option) in choices.iter().enumerate() {
let index = row * columns + column;
let response = picture_card(ui, &option.card(selected == index, enabled, size.aspect), width);
scenario::publish_target(&format!("prefs:{key}:{}", option.key), response.rect);
scenario::publish_target(&format!("dialog:{label}:{}", option.label), response.rect);
if !indexed && crate::prefs_search::texts_hit(ui, &[option.label]) {
crate::prefs_search::remember(ui, option.label, response.rect);
solidcraft_ui_registry::publish_target(&format!("prefs:{key}:{}", option.key), response.rect);
solidcraft_ui_registry::publish_target(&format!("dialog:{label}:{}", option.label), response.rect);
if !indexed && super::settings_search::texts_hit(ui, &[option.label]) {
super::settings_search::remember(ui, option.label, response.rect);
indexed = true;
}
crate::prefs_search::follow(ui, option.label, response.rect);
super::settings_search::follow(ui, option.label, response.rect);
if response.clicked() && enabled {
picked = Some(index);
}
@@ -311,7 +311,7 @@ pub(crate) fn settings_picture_choices_sized(
/// A toggle tile: the same picture with a checkbox in the corner. Click it to flip `card.selected`.
pub(crate) fn toggle_tile(ui: &mut Ui, card: &Card, width: f32) -> Response {
crate::prefs_search::note_unlabelled(ui, &[card.sub.unwrap_or("")]);
super::settings_search::note_unlabelled(ui, &[card.sub.unwrap_or("")]);
paint_card(ui, card, width, true)
}
@@ -326,9 +326,9 @@ pub(crate) fn preset_card(
icon: &dyn Fn(&Painter, Rect, Color32),
width: f32,
) -> Response {
crate::prefs_search::note_unlabelled(ui, &[name, word]);
if crate::prefs_search::querying(ui) && !crate::prefs_search::visible(ui, &[name, word]) {
return crate::prefs_search::hidden_response(ui);
super::settings_search::note_unlabelled(ui, &[name, word]);
if super::settings_search::querying(ui) && !super::settings_search::visible(ui, &[name, word]) {
return super::settings_search::hidden_response(ui);
}
let t = Tokens::get();
let enabled = enabled && ui.is_enabled();
@@ -336,8 +336,8 @@ pub(crate) fn preset_card(
let height = t.space_24 * 3.0 - t.space_2;
let (rect, response) = ui.allocate_exact_size(vec2(width, height), if enabled { Sense::click() } else { Sense::hover() });
if super::layout::label(ui).is_empty() {
crate::prefs_search::remember(ui, name, rect);
crate::prefs_search::follow(ui, name, rect);
super::settings_search::remember(ui, name, rect);
super::settings_search::follow(ui, name, rect);
}
let look = card_look(selected, enabled, response.hovered(), response.has_focus());
let dim = |c: Color32| c.gamma_multiply(look.alpha);
@@ -373,7 +373,7 @@ pub(crate) fn preset_card(
tick(p, Rect::from_center_size(centre, vec2(t.space_12, t.space_12)), dim(t.on_accent));
}
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::RadioButton, enabled, selected, name));
scenario::publish_target(&format!("prefs:preset:{name}"), rect);
solidcraft_ui_registry::publish_target(&format!("prefs:preset:{name}"), rect);
if enabled { response.on_hover_cursor(egui::CursorIcon::PointingHand) } else { response }
}
@@ -388,7 +388,7 @@ pub(crate) fn help_icon(ui: &mut Ui, text: &str) -> Response {
p.circle_filled(pos2(rect.center().x, rect.top() + size * 0.3), 0.9, ink);
p.line_segment([pos2(rect.center().x, rect.top() + size * 0.46), pos2(rect.center().x, rect.bottom() - size * 0.24)], Stroke::new(1.2, ink));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), text));
scenario::publish_target("prefs:help_icon", rect);
solidcraft_ui_registry::publish_target("prefs:help_icon", rect);
response.backing_on_hover_text(text)
}
@@ -439,10 +439,10 @@ pub(crate) fn settings_plugin_card(
let tile = Rect::from_center_size(pos2(rect.left() + tile_side / 2.0, header_center), vec2(tile_side, tile_side));
ui.painter().rect_filled(tile, t.radius_lg, t.selected_bg);
crate::icons::paint_ink(ui.painter(), tile.shrink(t.space_8), "settings_plug", t.form_primary);
scenario::publish_rect(&format!("prefs:plugin:{key}:icon"), tile);
solidcraft_ui_registry::publish_rect(&format!("prefs:plugin:{key}:icon"), tile);
let at = pos2(tile.right() + t.space_12, header_center - text_height / 2.0);
let title_rect = Rect::from_min_size(at, title.size());
scenario::publish_rect(&format!("prefs:plugin:{key}:title"), title_rect);
solidcraft_ui_registry::publish_rect(&format!("prefs:plugin:{key}:title"), title_rect);
let version_at = pos2(at.x + title.size().x + t.space_8, at.y + (header_height - version_galley.size().y) / 2.0);
ui.painter().galley(pos2(at.x, at.y + (header_height - title.size().y) / 2.0), title, t.form_ink);
ui.painter().galley(version_at, version_galley, t.form_dim);
@@ -230,7 +230,7 @@ pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutP
panel(inset, inset, 120.0 - inset * 2.0, ribbon_height, "", t.toolbar);
let browser =
panel(bx, top, bw, browser_height, "Browser", if state.translucent && !state.compact { t.panel.gamma_multiply(0.60) } else { t.panel });
crate::scenario::publish_rect("prefs:layout_preview:browser", browser);
solidcraft_ui_registry::publish_rect("prefs:layout_preview:browser", browser);
// A full row of quiet tool symbols, with label marks only when labels are enabled.
let quiet = t.form_dim.lerp_to_gamma(t.viewport_top, 0.55);
for index in 0..10 {
@@ -261,7 +261,7 @@ pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutP
_ => Some(panel(inset, bottom - 10.0, 120.0 - inset * 2.0, 10.0, "History", t.selected_bg)),
};
if let Some(history) = history {
crate::scenario::publish_rect("prefs:layout_preview:history", history);
solidcraft_ui_registry::publish_rect("prefs:layout_preview:history", history);
}
if let Some(history) = history.filter(|_| state.history == 1) {
// A single vertical mark reads as the narrow icon-only history rail.
@@ -279,8 +279,8 @@ pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutP
(120.0 - inset - bw, top, (viewport_bottom - top) * 0.65)
};
let dialog = panel(dialog_x, dialog_top, bw, dialog_height, "Dialogs", t.dialog_footer);
crate::scenario::publish_rect("prefs:layout_preview:dialog", dialog);
crate::scenario::publish_rect("prefs:layout_preview", outer);
solidcraft_ui_registry::publish_rect("prefs:layout_preview:dialog", dialog);
solidcraft_ui_registry::publish_rect("prefs:layout_preview", outer);
ui.interact(outer, ui.make_persistent_id("layout_preview_help"), egui::Sense::hover())
.backing_on_hover_text(crate::i18n::tr("prefs.layout_note"));
}
@@ -11,7 +11,7 @@ use egui::{FontId, Rect, Sense, Stroke, Ui, pos2, vec2};
pub(crate) fn settings_mouse_mapping(ui: &mut Ui, bindings: &[(Vec<String>, String)], mapping: impl FnOnce(&mut Ui)) {
let keys: Vec<Vec<&str>> = bindings.iter().map(|(keys, _)| keys.iter().map(String::as_str).collect()).collect();
let rows: Vec<(&[&str], &str)> = bindings.iter().zip(&keys).map(|((_, action), keys)| (keys.as_slice(), action.as_str())).collect();
if crate::prefs_search::suppress_chrome(ui) {
if super::settings_search::suppress_chrome(ui) {
mapping(ui);
super::settings_bindings(ui, &rows);
return;
@@ -59,7 +59,7 @@ fn axis_columns(rect: Rect) -> [Rect; 5] {
}
pub(crate) fn settings_axis_header(ui: &mut Ui) {
if crate::prefs_search::suppress_chrome(ui) {
if super::settings_search::suppress_chrome(ui) {
return;
}
let t = Tokens::get();
@@ -96,7 +96,7 @@ pub(crate) fn settings_axis_row(
super::layout::sized_row(ui, name, height, |ui| {
// The row already owns these bounds; its centered cursor is not the top edge.
let rect = ui.max_rect();
crate::scenario::publish_rect(&format!("prefs:axis:{name}"), rect);
solidcraft_ui_registry::publish_rect(&format!("prefs:axis:{name}"), rect);
t.secondary_hline(ui.painter(), rect.x_range(), rect.bottom(), t.dialog_rule);
if narrow {
let label_rect = Rect::from_min_size(rect.min, vec2(rect.width() * 0.5, line));
@@ -173,11 +173,10 @@ pub(crate) fn live_axis_bar_in(ui: &Ui, live_rect: Rect, value: f32, dead_zone:
/// Compact per-axis gain uses a consistent two-decimal specimen and the real accepted range.
pub(crate) fn settings_axis_gain(ui: &mut Ui, gain: &mut f32) -> egui::Response {
if crate::prefs_search::querying(ui) && (!ui.is_visible() || crate::prefs_search::suppress_chrome(ui)) {
return crate::prefs_search::hidden_response(ui);
if super::settings_search::querying(ui) && (!ui.is_visible() || super::settings_search::suppress_chrome(ui)) {
return super::settings_search::hidden_response(ui);
}
let response =
ui.add(egui::DragValue::new(gain).range(0.0..=3.0).speed(0.05).suffix("×").fixed_decimals(2).custom_parser(crate::prefs::parse_number));
let response = ui.add(egui::DragValue::new(gain).range(0.0..=3.0).speed(0.05).suffix("×").fixed_decimals(2).custom_parser(super::parse_number));
response.widget_info(|| {
let mut info = egui::WidgetInfo::drag_value(ui.is_enabled(), f64::from(*gain));
info.label = Some(super::layout::label(ui).to_owned());
+21 -21
View File
@@ -75,14 +75,14 @@ pub(crate) fn settings_profile_button(ui: &mut Ui, label: &str, name: &str, edit
let value = Rect::from_min_max(pos2(text_left, overline.bottom()), pos2(text_right, rect.center().y + t.space_16));
super::text::paint_text(ui, overline, &label.to_uppercase(), crate::fonts::semibold(t.font_panel), t.form_dim, true);
super::text::paint_text(ui, value, name, crate::fonts::semibold(t.font_label), t.form_ink, false);
crate::scenario::publish_rect("prefs:profile:icon", icon);
crate::scenario::publish_rect("prefs:profile:label", overline);
crate::scenario::publish_rect("prefs:profile:name", value);
crate::scenario::publish_rect("prefs:profile:chevron", chevron);
solidcraft_ui_registry::publish_rect("prefs:profile:icon", icon);
solidcraft_ui_registry::publish_rect("prefs:profile:label", overline);
solidcraft_ui_registry::publish_rect("prefs:profile:name", value);
solidcraft_ui_registry::publish_rect("prefs:profile:chevron", chevron);
if edited {
let dot = Rect::from_min_size(pos2(text_right + t.space_6, value.center().y - t.space_4), vec2(t.space_8, t.space_8));
let response = ui.scope_builder(egui::UiBuilder::new().max_rect(dot), |ui| settings_change_dot(ui, changed_hint)).inner;
crate::scenario::publish_target("prefs:profile:edited", response.rect);
solidcraft_ui_registry::publish_target("prefs:profile:edited", response.rect);
}
});
let response = ui.interact(card.response.rect, ui.make_persistent_id("profile_card"), Sense::click());
@@ -101,7 +101,7 @@ pub(crate) fn settings_profile_button(ui: &mut Ui, label: &str, name: &str, edit
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), name));
crate::scenario::publish_rect("prefs:profile:sidebar_card", response.rect);
solidcraft_ui_registry::publish_rect("prefs:profile:sidebar_card", response.rect);
response.backing_on_hover_text(label)
}
@@ -141,9 +141,9 @@ pub(crate) struct SettingsChip<'a> {
/// Bounded informational pill; an optional removal target lives inside the same outline.
pub(crate) fn settings_chip(ui: &mut Ui, label: &str, chip: SettingsChip<'_>) -> egui::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 ink = if chip.accent { t.selected_ink } else { t.form_dim };
@@ -161,8 +161,8 @@ pub(crate) fn settings_chip(ui: &mut Ui, label: &str, chip: SettingsChip<'_>) ->
let height = if chip.compact { t.space_24 - t.space_2 } else { t.space_24 };
let (rect, response) = ui.allocate_exact_size(vec2(natural.min(super::metrics::dimension(ui.max_rect().width())), height), Sense::hover());
if super::layout::label(ui).is_empty() {
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);
}
ui.painter().rect(
rect,
@@ -212,15 +212,15 @@ pub(crate) fn settings_block_hint<'a, R>(
}
fn settings_block_content<R>(ui: &mut Ui, label: &str, hint: Option<&str>, label_width: f32, add: impl FnOnce(&mut Ui) -> R) -> R {
if let Some(gate) = crate::prefs_search::block_gate(ui, label, hint) {
let before = crate::prefs_search::hits(ui);
if let Some(gate) = super::settings_search::block_gate(ui, label, hint) {
let before = super::settings_search::hits(ui);
if !gate.show {
let result = crate::prefs_search::run_hidden(ui, label, add);
crate::prefs_search::block_finish(ui, &gate, false, before);
let result = super::settings_search::run_hidden(ui, label, add);
super::settings_search::block_finish(ui, &gate, false, before);
return result;
}
let result = settings_block_body(ui, label, hint, label_width, add);
crate::prefs_search::block_finish(ui, &gate, true, before);
super::settings_search::block_finish(ui, &gate, true, before);
return result;
}
settings_block_body(ui, label, hint, label_width, add)
@@ -255,7 +255,7 @@ fn settings_block_body<R>(ui: &mut Ui, label: &str, hint: Option<&str>, label_wi
)
.inner
});
crate::scenario::publish_rect(&format!("dialog:row:{label}"), response.response.rect);
solidcraft_ui_registry::publish_rect(&format!("dialog:row:{label}"), response.response.rect);
response.inner
}
@@ -281,7 +281,7 @@ pub(crate) fn settings_autosave_timeline(ui: &mut Ui, minutes: f64) {
let (r, _) = ui.allocate_exact_size(vec2(super::metrics::dimension(ui.available_width()), t.space_24 + t.space_12 + t.space_2), Sense::hover());
let line = r;
if settings_page_active(ui) {
crate::scenario::publish_rect("prefs:autosave_timeline", r);
solidcraft_ui_registry::publish_rect("prefs:autosave_timeline", r);
}
ui.painter().hline(line.x_range(), r.top() + t.space_10, Stroke::new(2.0, t.dialog_rule));
if minutes.is_finite() && minutes > 0.0 {
@@ -324,11 +324,11 @@ pub(crate) fn settings_bindings(ui: &mut Ui, bindings: &[(&[&str], &str)]) {
})
.fold(t.space_24 * 8.0 + t.space_8, f32::max);
for (keys, action) in bindings {
if crate::prefs_search::querying(ui) && !crate::prefs_search::shows_all(ui) {
if !crate::prefs_search::texts_hit(ui, &[action, &keys.join(" ")]) {
if super::settings_search::querying(ui) && !super::settings_search::shows_all(ui) {
if !super::settings_search::texts_hit(ui, &[action, &keys.join(" ")]) {
continue;
}
crate::prefs_search::reveal(ui);
super::settings_search::reveal(ui);
}
let width = super::metrics::dimension(ui.available_width());
let action_width = super::ContentWidth::label(ui, action);
+3 -2
View File
@@ -1,5 +1,6 @@
//! Analytic rounded annular sectors, following the board's offset-and-round construction.
use crate::{context_menu::Item, theme::Tokens};
use super::Item;
use crate::theme::Tokens;
use egui::{Align2, Color32, FontId, Pos2, Rect, Sense, Stroke, Ui, Vec2, vec2};
use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU};
@@ -110,7 +111,7 @@ pub(crate) fn radial_menu(ui: &mut Ui, center: Pos2, items: &[Item], hot: Option
let at = center + unit(a) * 104.0;
let target = Rect::from_center_size(at, vec2(t.space_24 * 2.0 + t.space_12, t.space_24 + t.space_16 + t.space_2));
let response = ui.interact(target, ui.scope_id().with(("radial", k)), Sense::hover());
crate::scenario::publish_target(&format!("radial:{}", item.id), target);
solidcraft_ui_registry::publish_target(&format!("radial:{}", item.id), target);
let ink = if lit { accent.ink } else { t.form_ink };
crate::icons::paint_accented(
&painter,
@@ -1,6 +1,6 @@
//! The colours picked last in any colour field, newest first (#9234). The app keeps the list in
//! its saved settings (`UiState::recent_colors`) and hands it to the colour pickers every frame
//! through egui memory; the shared `ui_kit` colour popover shows it and adds finished picks.
//! The colours picked last in any colour field, newest first (#9234). The host keeps the list in
//! its saved settings and hands it to the colour pickers every frame through egui memory with
//! [`share`]; the kit's colour popover shows it and adds finished picks.
/// How many colours the recent list keeps.
pub(crate) const MAX_RECENT_COLORS: usize = 12;
+14 -14
View File
@@ -1,6 +1,6 @@
//! Read-only report and library containers, sharing the form's typography and actions.
use crate::theme::Tokens;
use crate::ui_kit::BackingTooltipExt as _;
use crate::{scenario, theme::Tokens};
use egui::{Ui, vec2};
/// Bordered list with a bounded scroll viewport. Tokens: dialog_rule, radius_md, space_10.
@@ -13,8 +13,8 @@ pub(crate) fn report_list<R>(ui: &mut Ui, key: &str, height: f32, add: impl FnOn
.auto_shrink([false, true])
.max_height(height)
.show(ui, add);
scenario::publish_rect(&format!("dialog:list:{key}:viewport"), output.inner_rect);
scenario::publish_count(&format!("dialog:list:{key}:content_width"), f64::from(output.content_size.x));
solidcraft_ui_registry::publish_rect(&format!("dialog:list:{key}:viewport"), output.inner_rect);
solidcraft_ui_registry::publish_count(&format!("dialog:list:{key}:content_width"), f64::from(output.content_size.x));
output.inner
})
}
@@ -26,7 +26,7 @@ pub(crate) fn report_divider(ui: &mut Ui, key: &str) {
let rect = rect.expand2(vec2(t.space_10, 0.0));
let clip = ui.clip_rect().expand2(vec2(t.space_10, 0.0));
ui.painter().with_clip_rect(clip).hline(rect.x_range(), rect.top(), t.tertiary_border(t.dialog_rule));
scenario::publish_rect(&format!("dialog:divider:{key}"), rect);
solidcraft_ui_registry::publish_rect(&format!("dialog:divider:{key}"), rect);
}
/// List border around a caller-owned virtualised viewport. Tokens: dialog_rule, control_radius, space_10.
@@ -49,7 +49,7 @@ pub(super) fn list_frame_filled<R>(ui: &mut Ui, key: &str, fill: egui::Color32,
add(ui)
},
);
scenario::publish_rect(&format!("dialog:list:{key}"), response.response.rect);
solidcraft_ui_registry::publish_rect(&format!("dialog:list:{key}"), response.response.rect);
response.inner
}
@@ -70,18 +70,18 @@ pub(crate) fn path_chip(ui: &mut Ui, path: &str, add: impl FnOnce(&mut Ui)) {
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), path));
response.backing_on_hover_text(path);
let key = format!("dialog:cell:{path}");
scenario::publish_handle(&key, rect.center());
scenario::publish_rect(&key, rect);
solidcraft_ui_registry::publish_handle(&key, rect.center());
solidcraft_ui_registry::publish_rect(&key, rect);
});
});
}
/// Empty-list dashed outline and centred caption. Retains the note rectangle.
pub(crate) fn empty_box(ui: &mut Ui, key: &str, message: &str) {
if crate::prefs_search::querying(ui) {
if crate::prefs_search::texts_hit(ui, &[message]) {
crate::prefs_search::reveal(ui);
} else if !ui.is_visible() || crate::prefs_search::suppress_chrome(ui) {
if super::settings_search::querying(ui) {
if super::settings_search::texts_hit(ui, &[message]) {
super::settings_search::reveal(ui);
} else if !ui.is_visible() || super::settings_search::suppress_chrome(ui) {
return;
}
}
@@ -94,7 +94,7 @@ pub(crate) fn empty_box(ui: &mut Ui, key: &str, message: &str) {
}
ui.painter().extend(egui::Shape::dashed_line(&points, t.control_border_stroke(t.control_border), t.space_4, t.space_3));
ui.painter().text(rect.center(), egui::Align2::CENTER_CENTER, message, egui::FontId::proportional(t.font_label), t.form_dim);
scenario::publish_rect(&format!("dialog:note:{key}"), rect);
solidcraft_ui_registry::publish_rect(&format!("dialog:note:{key}"), rect);
}
/// Kind/count above UTC time, alongside actions. The legacy combined-title cell remains an alias.
@@ -138,8 +138,8 @@ pub(crate) fn report_header(ui: &mut Ui, kind: &str, count: u64, time: &str, ali
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), alias));
response.backing_on_hover_text(alias);
let key = format!("dialog:cell:{alias}");
scenario::publish_rect(&key, rect);
scenario::publish_handle(&key, rect.center());
solidcraft_ui_registry::publish_rect(&key, rect);
solidcraft_ui_registry::publish_handle(&key, rect.center());
});
});
}
@@ -1,4 +1,5 @@
use super::*;
use crate::scenario;
use crate::ui_kit::{self, test_support as harness};
#[test]
+1 -1
View File
@@ -65,6 +65,6 @@ impl ReviewAndApply<'_> {
});
}
let ack = checkbox(ui, self.acknowledgement, acknowledged);
crate::scenario::publish_target("render:review:acknowledge", ack.rect);
solidcraft_ui_registry::publish_target("render:review:acknowledge", ack.rect);
}
}
+10 -10
View File
@@ -122,9 +122,9 @@ impl CommandSearch {
// Closed with less room than its hint takes (a narrow bar): just the magnifier.
if !open && progress == 0.0 && r.width() + 0.5 < Self::closed_width(ui.ctx()) {
icons::paint_icon(ui.painter(), Rect::from_center_size(r.center(), vec2(t.space_16, t.space_16)), "zoom", 1.0);
crate::scenario::publish_target("ribbon:search", r);
crate::scenario::publish_target("shortcut:search", r);
crate::scenario::publish_target("palette:search", r);
solidcraft_ui_registry::publish_target("ribbon:search", r);
solidcraft_ui_registry::publish_target("shortcut:search", r);
solidcraft_ui_registry::publish_target("palette:search", r);
return ui
.interact(r, egui::Id::unique("sc_sbox_compact"), Sense::click())
.backing_on_hover_text(crate::i18n::tr_label("Search commands"));
@@ -169,9 +169,9 @@ impl CommandSearch {
if ui.rect_contains_pointer(r) {
ui.ctx().set_cursor_icon(egui::CursorIcon::Text);
}
crate::scenario::publish_target("ribbon:search", r);
crate::scenario::publish_target("shortcut:search", r);
crate::scenario::publish_target("palette:search", r);
solidcraft_ui_registry::publish_target("ribbon:search", r);
solidcraft_ui_registry::publish_target("shortcut:search", r);
solidcraft_ui_registry::publish_target("palette:search", r);
response
}
@@ -239,8 +239,8 @@ impl CommandSearch {
ui.painter().rect_stroke(sheet, t.radius_xl, frame.stroke, egui::StrokeKind::Inside);
super::Container::paint_inset(ui.painter(), sheet, frame);
ui.allocate_rect(sheet, Sense::hover());
crate::scenario::publish_target("ribbon:search_popup", sheet);
crate::scenario::publish_target("palette:outer", sheet);
solidcraft_ui_registry::publish_target("ribbon:search_popup", sheet);
solidcraft_ui_registry::publish_target("palette:outer", sheet);
sheet
}
@@ -257,7 +257,7 @@ impl CommandSearch {
let frame = egui::Frame::NONE.fill(t.dialog_footer).corner_radius(egui::CornerRadius { nw: 0, ne: 0, sw: t.radius_xl, se: t.radius_xl });
super::Container::set_backing(ui.painter(), background, frame, r);
t.secondary_hline(ui.painter(), r.x_range(), r.top(), t.dialog_rule);
crate::scenario::publish_rect("ribbon:search_footer", r);
solidcraft_ui_registry::publish_rect("ribbon:search_footer", r);
}
}
@@ -358,7 +358,7 @@ impl super::MenuItem<'_> {
super::text::paint_text(ui, path_rect, path, FontId::proportional(t.font_small), t.form_dim, false);
}
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, self.enabled, label));
crate::scenario::publish_target(&format!("menu:{}", self.id), r);
solidcraft_ui_registry::publish_target(&format!("menu:{}", self.id), r);
response
}
}
@@ -1,7 +1,8 @@
//! Search across Preferences. The field is the Keyboard Shortcuts magnifier. A match is the
//! control's own label, its section, its page, or the value a dropdown is showing, scored the
//! same way as command search. English counts too, from the message catalog, so a new setting
//! is found by being drawn — nothing is registered for search.
//! Search across a settings page the kit draws; the Preferences window is SolidCraft's use of it.
//! The field is the Keyboard Shortcuts magnifier. A match is the control's own label, its
//! section, its page, or the value a dropdown is showing, scored the same way as command search.
//! English counts too, from the message catalog, so a new setting is found by being drawn —
//! nothing is registered for search.
use std::collections::{HashMap, HashSet};
@@ -801,7 +802,7 @@ pub(crate) fn remember(ui: &mut Ui, label: &str, rect: Rect) {
if scroll {
ui.scroll_to_rect(rect, Some(egui::Align::Center));
}
crate::scenario::publish_handle("prefs:search:current", rect.center());
solidcraft_ui_registry::publish_handle("prefs:search:current", rect.center());
}
pub(crate) fn set_reveal(ctx: &Context, reveal: Option<Reveal>) {
@@ -815,13 +816,13 @@ pub(crate) fn set_reveal(ctx: &Context, reveal: Option<Reveal>) {
}
pub(crate) fn reveal_for(page: usize, label: &str) -> Reveal {
Reveal { page, label: label.to_string(), until: crate::now_ms() + REVEAL_MS }
Reveal { page, label: label.to_string(), until: now_ms() + REVEAL_MS }
}
/// Scroll the opened setting into view and outline it until [`Reveal::until`].
pub(crate) fn follow(ui: &Ui, label: &str, rect: Rect) {
let Some(reveal) = ui.data(|data| data.get_temp::<Reveal>(Id::unique(REVEAL_ID))) else { return };
if crate::now_ms() > reveal.until || fold(label) != fold(&reveal.label) {
if now_ms() > reveal.until || fold(label) != fold(&reveal.label) {
return;
}
let page = with(ui, |state| state.page_index);
@@ -831,7 +832,13 @@ pub(crate) fn follow(ui: &Ui, label: &str, rect: Rect) {
ui.scroll_to_rect(rect, Some(egui::Align::Center));
let t = Tokens::get();
ui.painter().rect_stroke(rect, t.radius_sm, egui::Stroke::new(t.space_2, t.form_primary), egui::StrokeKind::Outside);
crate::scenario::publish_handle("prefs:revealed", rect.center());
solidcraft_ui_registry::publish_handle("prefs:revealed", rect.center());
}
/// Wall clock in milliseconds, for how long a reveal stays outlined.
fn now_ms() -> f64 {
use web_time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
}
#[cfg(test)]
+2 -2
View File
@@ -84,7 +84,7 @@ fn search_with_icon(ui: &mut Ui, id: egui::Id, value: &mut String, hint: &str) -
icons::paint_ink(ui.painter(), icon, "zoom", t.form_dim);
let show_clear = !value.is_empty();
let clear_side = if show_clear { height } else { super::CommandSearch::key(ui, r.right_center() - vec2(t.space_6, 0.0), "Ctrl F") + t.space_12 };
crate::scenario::publish_rect("prefs:search:frame", r);
solidcraft_ui_registry::publish_rect("prefs:search:frame", r);
let editor =
Rect::from_min_max(pos2((icon.right() + t.space_6).min(r.right()), r.top()), pos2((r.right() - clear_side).max(icon.right()), r.bottom()));
let response = ui
@@ -110,7 +110,7 @@ fn search_with_icon(ui: &mut Ui, id: egui::Id, value: &mut String, hint: &str) -
let clear_rect = Rect::from_center_size(pos2(r.right() - height * 0.5, r.center().y), vec2(height, height));
let clear = ui.push_id(id.with("clear"), |ui| super::quiet_icon_button_at(ui, clear_rect, "close", "Clear")).inner;
if id == egui::Id::unique("prefs_search") {
crate::scenario::publish_handle("prefs:search:clear", clear_rect.center());
solidcraft_ui_registry::publish_handle("prefs:search:clear", clear_rect.center());
}
if clear.clicked() {
value.clear();
+9 -9
View File
@@ -1,6 +1,6 @@
//! Small deterministic material previews; no textures, jobs or graphics device.
use crate::theme::Tokens;
use crate::ui_kit::BackingTooltipExt as _;
use crate::{scenario, theme::Tokens};
use egui::{Color32, Mesh, Rect, Response, Sense, Ui, pos2, vec2};
use std::{collections::VecDeque, sync::Arc};
@@ -171,8 +171,8 @@ pub fn sphere_thumbnail(ui: &mut Ui, id: &str, sphere: Sphere, size: f32) -> Res
let side = f32::from(sphere.key(size).side);
let (rect, response) = ui.allocate_exact_size(vec2(side, side), Sense::hover());
paint_sphere(ui, rect, sphere);
scenario::publish_rect(id, rect);
scenario::publish_handle(id, rect.center());
solidcraft_ui_registry::publish_rect(id, rect);
solidcraft_ui_registry::publish_handle(id, rect.center());
response
}
@@ -221,16 +221,16 @@ pub(crate) fn sphere_preview_with_source<R>(
);
source_ui.set_clip_rect(ui.clip_rect().intersect(source_rect));
let source = source(&mut source_ui);
scenario::publish_rect(&format!("{id}:chip:0"), source.rect.intersect(source_rect));
solidcraft_ui_registry::publish_rect(&format!("{id}:chip:0"), source.rect.intersect(source_rect));
let mut usage_ui = ui.new_child(
egui::UiBuilder::new().id_salt("sample_usage").max_rect(usage_rect).layout(egui::Layout::left_to_right(egui::Align::Center)),
);
usage_ui.set_clip_rect(ui.clip_rect().intersect(usage_rect));
let response = super::workspace_chip(&mut usage_ui, usage);
scenario::publish_rect(&format!("{id}:chip:1"), response.rect.intersect(usage_rect));
solidcraft_ui_registry::publish_rect(&format!("{id}:chip:1"), response.rect.intersect(usage_rect));
});
let response = ui.vertical(action);
scenario::publish_target(&format!("{id}:replace"), response.response.rect);
solidcraft_ui_registry::publish_target(&format!("{id}:replace"), response.response.rect);
response.inner
});
row.inner
@@ -291,11 +291,11 @@ impl SphereTile<'_> {
job.wrap.break_anywhere = false;
let galley = ui.painter().layout_job(job);
let label_pos = pos2(label.center().x - galley.rect.center().x, label.top() - galley.rect.top());
scenario::publish_rect(&format!("{}:label", self.id), label);
scenario::publish_rect(&format!("{}:galley", self.id), galley.rect.translate(label_pos.to_vec2()));
solidcraft_ui_registry::publish_rect(&format!("{}:label", self.id), label);
solidcraft_ui_registry::publish_rect(&format!("{}:galley", self.id), galley.rect.translate(label_pos.to_vec2()));
ui.painter().with_clip_rect(ui.clip_rect().intersect(label)).galley(label_pos, galley, t.form_ink);
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), self.selected, self.name));
scenario::publish_target(self.id, rect);
solidcraft_ui_registry::publish_target(self.id, rect);
let mut tip = egui::Tooltip::for_enabled(&response);
tip.popup = tip.popup.frame(super::Container::new(ui.ctx(), super::Surface::Toast).tooltip_frame(ui.ctx()));
crate::ui_kit::Container::show_tooltip(response.id, tip, |ui| {
+10 -10
View File
@@ -37,12 +37,12 @@ pub(crate) fn status_header(ui: &mut egui::Ui, key: &str, name: &str, status: St
if fraction > 0.0 {
ui.painter().rect_filled(egui::Rect::from_min_size(meter.min, vec2(meter.width() * fraction, meter.height())), t.space_3, t.form_ink);
}
crate::scenario::publish_rect(&format!("{key}:meter"), meter);
crate::scenario::publish_count(&format!("{key}:constrained_fraction"), f64::from(fraction));
solidcraft_ui_registry::publish_rect(&format!("{key}:meter"), meter);
solidcraft_ui_registry::publish_count(&format!("{key}:constrained_fraction"), f64::from(fraction));
})
.response;
t.secondary_hline(ui.painter(), ui.max_rect().x_range(), ui.cursor().top(), t.dialog_rule);
crate::scenario::publish_target(&format!("{key}:status_header"), response.rect);
solidcraft_ui_registry::publish_target(&format!("{key}:status_header"), response.rect);
response
}
@@ -58,7 +58,7 @@ pub(crate) fn progress(ui: &mut egui::Ui, key: &str, fraction: f32) -> egui::Res
ui.painter().rect_filled(egui::Rect::from_min_size(bar.min, vec2(bar.width() * fraction, bar.height())), t.space_3, t.form_primary);
}
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::ProgressIndicator, ui.is_enabled(), format!("{:.0}%", fraction * 100.0)));
crate::scenario::publish_rect(&format!("dialog:progress:{key}"), bar);
solidcraft_ui_registry::publish_rect(&format!("dialog:progress:{key}"), bar);
response
}
@@ -132,8 +132,8 @@ pub(crate) fn steps(ui: &mut egui::Ui, key: &str, labels: &[&str], current: usiz
std::cmp::Ordering::Greater => "next",
};
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), *label));
crate::scenario::publish_rect(&format!("dialog:step:{key}:{i}:{state}"), row);
crate::scenario::publish_rect(&format!("dialog:step_mark:{key}:{i}"), slot);
solidcraft_ui_registry::publish_rect(&format!("dialog:step:{key}:{i}:{state}"), row);
solidcraft_ui_registry::publish_rect(&format!("dialog:step_mark:{key}:{i}"), slot);
}
}
@@ -210,7 +210,7 @@ pub(crate) fn check_status_line(ui: &mut egui::Ui, key: &str, label: &str, statu
super::text(ui, label, super::Text::Report(key, false));
})
.response;
crate::scenario::publish_rect(key, response.rect);
solidcraft_ui_registry::publish_rect(key, response.rect);
response
}
@@ -240,14 +240,14 @@ pub(crate) fn progress_action(ui: &mut egui::Ui, key: &str, name: &str, status:
};
let shown = fixed(ui, name_width, &|ui| super::text(ui, name, super::Text::Inline));
let cut = name_natural > name_width + 0.5;
crate::scenario::publish_rect(&format!("{key}:name"), shown.rect);
crate::scenario::publish_count(&format!("{key}:name:cut"), f64::from(u8::from(cut)));
solidcraft_ui_registry::publish_rect(&format!("{key}:name"), shown.rect);
solidcraft_ui_registry::publish_count(&format!("{key}:name:cut"), f64::from(u8::from(cut)));
fixed(ui, status_width, &|ui| super::text(ui, status, super::Text::Inline));
fixed(ui, meter, &|ui| progress(ui, key, fraction));
// Cancel keeps its whole label whatever room the row has left.
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let response = super::button(ui, cancel, super::Button::Normal);
crate::scenario::publish_rect(&format!("{key}:cancel"), response.rect);
solidcraft_ui_registry::publish_rect(&format!("{key}:cancel"), response.rect);
response
})
.inner
+16 -16
View File
@@ -1,8 +1,8 @@
//! Dense editable tables. Geometry and typography share the form label-column tokens.
use super::metrics::*;
use crate::theme::Tokens;
use crate::ui_kit::BackingTooltipExt as _;
use crate::ui_kit::{Container, Surface};
use crate::{scenario, theme::Tokens};
use egui::{Response, Ui, vec2};
/// A column's minimum width is a multiple of the measured form label column.
@@ -78,8 +78,8 @@ pub(crate) fn table_custom<R>(ui: &mut Ui, id: &str, columns: &[Column<'_>], emp
result
},
);
scenario::publish_rect(&format!("dialog:table:{id}:viewport"), output.inner_rect);
scenario::publish_count(&format!("dialog:table:{id}:content_width"), f64::from(output.content_size.x));
solidcraft_ui_registry::publish_rect(&format!("dialog:table:{id}:viewport"), output.inner_rect);
solidcraft_ui_registry::publish_count(&format!("dialog:table:{id}:content_width"), f64::from(output.content_size.x));
// The nested horizontal ScrollArea consumes vertical scroll targets too. Request the
// reveal here so it reaches the card's vertical scroller, only after an explicit action.
if let Some(rect) = reveal {
@@ -165,7 +165,7 @@ impl Table<'_> {
}
},
);
scenario::publish_rect(&format!("dialog:column:{}:{}", self.id, column.label), cell);
solidcraft_ui_registry::publish_rect(&format!("dialog:column:{}:{}", self.id, column.label), cell);
left = cell.right() + t.space_8;
}
ui.painter().hline(rect.x_range(), rect.bottom(), t.tertiary_border(t.dialog_rule));
@@ -217,8 +217,8 @@ impl Table<'_> {
false,
);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), crate::i18n::tr_label(label)));
scenario::publish_handle(&format!("dialog:table:{}:add", self.id), rect.center());
scenario::publish_rect(&format!("dialog:table:{}:add", self.id), rect);
solidcraft_ui_registry::publish_handle(&format!("dialog:table:{}:add", self.id), rect.center());
solidcraft_ui_registry::publish_rect(&format!("dialog:table:{}:add", self.id), rect);
response.on_hover_cursor(egui::CursorIcon::PointingHand).backing_on_hover_text(crate::i18n::tr_label(tooltip))
}
@@ -232,7 +232,7 @@ impl Table<'_> {
ui.painter().rect_filled(rect, t.control_radius, if selected { t.selected_bg } else { t.hover });
}
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), selected, key));
scenario::publish_rect(&format!("dialog:table:{}:{key}", self.id), rect);
solidcraft_ui_registry::publish_rect(&format!("dialog:table:{}:{key}", self.id), rect);
let ppp = ui.ctx().pixels_per_point().max(0.5);
let left = (rect.left() * ppp).round() / ppp + t.space_8;
ui.push_id((&self.id, key), |ui| {
@@ -267,7 +267,7 @@ impl TableRow<'_> {
let width = self.widths.get(self.column).copied().unwrap_or(0.0);
let label = self.columns.get(self.column).map_or("", |c| c.label);
let rect = egui::Rect::from_min_size(egui::pos2(self.left, self.rect.top()), vec2(width, self.rect.height()));
scenario::publish_rect(&format!("{}:cell:{label}", self.id), rect);
solidcraft_ui_registry::publish_rect(&format!("{}:cell:{label}", self.id), rect);
self.column += 1;
self.left += width + Tokens::get().space_8;
self.ui
@@ -413,8 +413,8 @@ pub(crate) fn removable_header(ui: &mut Ui, label: &str) -> bool {
return false;
}
let response = super::actions::icon_button_colored(ui, button, "close", "Remove the column", Some(t.form_error));
scenario::publish_handle(&format!("dialog:button:{label}:×"), button.center());
scenario::publish_rect(&format!("dialog:button:{label}:×"), button);
solidcraft_ui_registry::publish_handle(&format!("dialog:button:{label}:×"), button.center());
solidcraft_ui_registry::publish_rect(&format!("dialog:button:{label}:×"), button);
response.clicked()
}
@@ -427,8 +427,8 @@ pub(crate) fn bin_button(ui: &mut Ui, tooltip: &str) -> Response {
[("Actions", "Delete"), ("Actions", "×"), (super::layout::label(ui), crate::i18n::tr_label("Delete")), (super::layout::label(ui), "×")]
{
let key = format!("dialog:button:{row}:{label}");
scenario::publish_handle(&key, rect.center());
scenario::publish_rect(&key, rect);
solidcraft_ui_registry::publish_handle(&key, rect.center());
solidcraft_ui_registry::publish_rect(&key, rect);
}
response
}
@@ -447,8 +447,8 @@ pub(crate) fn plus_header<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> Opt
ui.painter().vline(x, rect.y_range(), egui::Stroke::new(t.space_2, t.form_primary));
}
}
scenario::publish_handle("dialog:menu:Add Column", rect.center());
scenario::publish_rect("dialog:menu:Add Column", rect);
solidcraft_ui_registry::publish_handle("dialog:menu:Add Column", rect.center());
solidcraft_ui_registry::publish_rect("dialog:menu:Add Column", rect);
let popup = Container::new(ui.ctx(), Surface::Popup).menu(&response).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside);
let open = popup.is_open();
let result = popup.show(|ui| super::scope(ui, add)).map(|r| r.inner);
@@ -467,8 +467,8 @@ pub(crate) fn active_pill(ui: &mut Ui) {
ui.painter().rect(rect, t.radius_xl, t.control_fill, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
super::text::paint_text(ui, rect.shrink2(vec2(t.space_8, 0.0)), label, crate::fonts::semibold(t.font_small), t.form_primary, false);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), label));
scenario::publish_handle(&format!("dialog:inline:{label}"), rect.center());
scenario::publish_rect(&format!("dialog:inline:{label}"), rect);
solidcraft_ui_registry::publish_handle(&format!("dialog:inline:{label}"), rect.center());
solidcraft_ui_registry::publish_rect(&format!("dialog:inline:{label}"), rect);
}
#[cfg(test)]
+1
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::scenario;
use crate::ui_kit::{self, test_support as harness};
#[test]
+1 -1
View File
@@ -249,7 +249,7 @@ pub(crate) fn menu(ui: &Ui, response: &mut Response, text: &mut dyn TextBuffer,
};
let mut picked = None;
let open = super::Menu::context(response, |ui| {
super::Menu::show(ui, crate::context_menu::LIST_W, |ui| {
super::Menu::show(ui, super::LIST_W, |ui| {
for edit in Edit::ALL {
let (row, message, icon, key) = edit.row();
let shortcut = crate::keymap::display(key);
+21 -21
View File
@@ -1,6 +1,6 @@
//! Shared theme browser vocabulary: toggle segments, slots, marked cards and scroll viewport.
use super::*;
use crate::{scenario, theme::Tokens};
use crate::theme::Tokens;
use egui::{Align2, Color32, FontId, Painter, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
#[derive(Clone, Copy, PartialEq)]
@@ -121,8 +121,8 @@ impl<'a> ThemeSegment<'a> {
pub(crate) fn theme_segments(ui: &mut Ui, key: &str, options: &[ThemeSegment<'_>], selected: Option<usize>, icon_only: bool) -> Option<usize> {
let labels: Vec<_> = options.iter().map(|o| o.label).collect();
crate::prefs_search::note_unlabelled(ui, &labels);
if crate::prefs_search::querying(ui) && !crate::prefs_search::visible(ui, &labels) {
super::settings_search::note_unlabelled(ui, &labels);
if super::settings_search::querying(ui) && !super::settings_search::visible(ui, &labels) {
return None;
}
let t = Tokens::get();
@@ -188,11 +188,11 @@ pub(crate) fn theme_segments(ui: &mut Ui, key: &str, options: &[ThemeSegment<'_>
ui.painter().rect_stroke(r.expand(t.space_2), rounding, Stroke::new(t.space_2, t.form_primary), StrokeKind::Outside);
}
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), on, option.label));
crate::prefs_search::remember(ui, option.label, r);
crate::prefs_search::follow(ui, option.label, r);
super::settings_search::remember(ui, option.label, r);
super::settings_search::follow(ui, option.label, r);
let visible = r.intersect(ui.clip_rect());
if visible.is_positive() {
scenario::publish_target(&format!("prefs:theme_{key}:{index}"), visible);
solidcraft_ui_registry::publish_target(&format!("prefs:theme_{key}:{index}"), visible);
}
if response.clicked() {
clicked = Some(index);
@@ -239,7 +239,7 @@ pub(crate) fn theme_toolbar<L, R>(
let b = trailing(&mut right);
let height = if stacked { left_height + gap + right.min_size().y } else { left_height.max(right.min_size().y) };
let (rect, _) = ui.allocate_exact_size(vec2(width, height), Sense::hover());
scenario::publish_rect(&format!("prefs:theme_toolbar:{key}"), rect);
solidcraft_ui_registry::publish_rect(&format!("prefs:theme_toolbar:{key}"), rect);
(a, b)
}
@@ -249,9 +249,9 @@ fn bounded_width(ui: &Ui) -> f32 {
}
pub(crate) fn theme_import_button(ui: &mut Ui, label: &str) -> 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 width = ContentWidth::name(ui, label) + t.space_14 + t.space_6 + t.space_10 * 2.0;
@@ -270,8 +270,8 @@ pub(crate) fn theme_import_button(ui: &mut Ui, label: &str) -> Response {
ui.painter().rect_stroke(r.expand(t.space_2), t.radius_md, Stroke::new(t.space_2, t.form_primary), StrokeKind::Outside);
}
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), label));
crate::prefs_search::remember(ui, label, r);
crate::prefs_search::follow(ui, label, r);
super::settings_search::remember(ui, label, r);
super::settings_search::follow(ui, label, r);
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
@@ -286,9 +286,9 @@ pub(crate) struct ThemeSlot<'a> {
}
pub(crate) fn theme_slot(ui: &mut Ui, slot: &ThemeSlot<'_>) -> Response {
let t = Tokens::get();
crate::prefs_search::note_unlabelled(ui, &[slot.title, slot.name, slot.hint]);
if crate::prefs_search::querying(ui) && !crate::prefs_search::visible(ui, &[slot.title, slot.name, slot.hint]) {
return crate::prefs_search::hidden_response(ui);
super::settings_search::note_unlabelled(ui, &[slot.title, slot.name, slot.hint]);
if super::settings_search::querying(ui) && !super::settings_search::visible(ui, &[slot.title, slot.name, slot.hint]) {
return super::settings_search::hidden_response(ui);
}
let (r, response) = ui.allocate_exact_size(vec2(slot.width.max(0.0), t.space_24 * 2.0 + t.space_16), Sense::click());
ui.painter().rect(
@@ -308,8 +308,8 @@ pub(crate) fn theme_slot(ui: &mut Ui, slot: &ThemeSlot<'_>) -> Response {
p.text(pos2(x, r.bottom() - t.space_16 - t.space_4), Align2::LEFT_CENTER, slot.name, crate::fonts::semibold(t.font_label), t.form_ink);
p.text(pos2(r.right() - t.space_10, r.center().y), Align2::RIGHT_CENTER, slot.hint, FontId::proportional(t.font_small), t.form_dim);
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), slot.aimed, slot.title));
crate::prefs_search::remember(ui, slot.title, r);
crate::prefs_search::follow(ui, slot.title, r);
super::settings_search::remember(ui, slot.title, r);
super::settings_search::follow(ui, slot.title, r);
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
@@ -400,7 +400,7 @@ pub(crate) fn theme_empty(ui: &mut Ui, message: &str, clear: &str, reset: impl F
reset();
}
});
scenario::publish_rect("prefs:theme_empty", ui.min_rect());
solidcraft_ui_registry::publish_rect("prefs:theme_empty", ui.min_rect());
});
}
@@ -485,7 +485,7 @@ pub(crate) fn theme_browser(ui: &mut Ui, id: &str, count: usize, grid: bool, rev
offset = out.state.clamped_offset().x;
let r = out.inner_rect;
let hovered = ui.rect_contains_pointer(r);
let wheel = if hovered { crate::navigation::take_horizontal_wheel(ui, offset, max) } else { 0.0 };
let wheel = if hovered { super::wheel::take_horizontal_wheel(ui, offset, max) } else { 0.0 };
let horizontal_wheel = wheel != 0.0;
if horizontal_wheel {
offset = (offset - wheel).clamp(0.0, max);
@@ -532,7 +532,7 @@ pub(crate) fn theme_browser(ui: &mut Ui, id: &str, count: usize, grid: bool, rev
}
let label = crate::i18n::tr(if left { "prefs.appearance.previous_themes" } else { "prefs.appearance.next_themes" });
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), label));
scenario::publish_target(if left { "prefs:theme_previous" } else { "prefs:theme_next" }, arrow);
solidcraft_ui_registry::publish_target(if left { "prefs:theme_previous" } else { "prefs:theme_next" }, arrow);
if response.clicked() {
// A screenful: the cards that fit in view, at least one (#11434).
let page = (r.width() / stride).floor().max(1.0) * stride;
@@ -544,6 +544,6 @@ pub(crate) fn theme_browser(ui: &mut Ui, id: &str, count: usize, grid: bool, rev
ui.ctx().request_repaint();
}
ui.data_mut(|d| d.insert_temp(key, offset));
scenario::publish_rect("prefs:theme_viewport", r);
solidcraft_ui_registry::publish_rect("prefs:theme_viewport", r);
});
}
+13 -10
View File
@@ -174,7 +174,7 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
let mut x = r.left() + if row.compact { t.space_8 + row.depth as f32 * t.space_14 } else { 2.0 + row.depth as f32 * INDENT };
let cy = r.top() + row_height / 2.0;
// Where the row's depth puts it, whatever controls it has.
crate::scenario::publish_handle(&format!("indent:{}", row.label), pos2(x, cy));
solidcraft_ui_registry::publish_handle(&format!("indent:{}", row.label), pos2(x, cy));
let action_width = if row.compact { t.space_16 } else { t.form_close_size };
let action_left = (r.right() - if row.compact { t.space_8 } else { t.space_4 } - action_width * row.actions.len() as f32).max(r.left());
for (index, action) in row.actions.iter().enumerate() {
@@ -198,9 +198,9 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
Some(reason) => response.backing_on_disabled_hover_text(format!("{}\n{reason}", action.label)),
None => response.backing_on_hover_text(action.label),
};
crate::scenario::publish_rect(action.target, rect);
solidcraft_ui_registry::publish_rect(action.target, rect);
if action.reason.is_none() {
crate::scenario::publish_target(action.target, rect);
solidcraft_ui_registry::publish_target(action.target, rect);
}
out.actions.push(response);
}
@@ -249,7 +249,7 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
crate::ui_kit::disclosure(ui.painter(), pos2(x + 6.0, cy), open, if hover_on == 1 { t.text } else { t.text_dim });
out.fold = resp.clicked() && click_on == 1;
out.fold_at = Some(fold_r.center());
crate::scenario::publish_handle(&format!("fold:{}", row.label), fold_r.center());
solidcraft_ui_registry::publish_handle(&format!("fold:{}", row.label), fold_r.center());
}
x += if row.compact { t.space_16 } else { 14.0 };
// Eye.
@@ -281,7 +281,7 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
}
out.eye_rect = Some(eye_r.intersect(ui.clip_rect()));
out.eye_at = Some(er.center());
crate::scenario::publish_handle(&format!("eye:{}", row.label), er.center());
solidcraft_ui_registry::publish_handle(&format!("eye:{}", row.label), er.center());
}
if !row.compact {
x += slot;
@@ -332,7 +332,7 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
right -= t.space_16;
let sr = Rect::from_center_size(pos2(right + t.space_8, cy), vec2(t.space_14, t.space_14));
icons::paint_icon(ui.painter(), sr, status.icon(), 1.0);
crate::scenario::publish_handle(&format!("status:{}:{}", status.icon(), row.label), sr.center());
solidcraft_ui_registry::publish_handle(&format!("status:{}:{}", status.icon(), row.label), sr.center());
right -= t.space_4;
}
for text in [row.count, row.secondary, if row.compact { row.subtitle } else { "" }] {
@@ -367,7 +367,10 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
let galley = ui.painter().layout_job(job);
let lw = galley.size().x;
let elided = galley.elided;
crate::scenario::publish_rect(&format!("row_label:{}", row.label), Rect::from_min_size(pos2(x, cy - galley.size().y / 2.0), galley.size()));
solidcraft_ui_registry::publish_rect(
&format!("row_label:{}", row.label),
Rect::from_min_size(pos2(x, cy - galley.size().y / 2.0), galley.size()),
);
ui.painter().with_clip_rect(ui.painter().clip_rect().intersect(r)).galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
if row.cut {
let inside = r.shrink2(vec2(t.space_2, t.space_2));
@@ -396,9 +399,9 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
let ir = Rect::from_center_size(action_r.center(), vec2(t.space_14, t.space_14));
icons::paint_icon(ui.painter(), ir, icon, if action_hover { 1.0 } else { 0.6 });
out.action = resp.clicked() && action_click;
crate::scenario::publish_handle(&format!("action:{}", row.label), action_r.center());
solidcraft_ui_registry::publish_handle(&format!("action:{}", row.label), action_r.center());
}
crate::scenario::publish_handle(&format!("row:{}", row.label), r.center());
solidcraft_ui_registry::publish_handle(&format!("row:{}", row.label), r.center());
let tip = match hover_on {
0 if action_hover => row.action.map(|(_, tip)| crate::i18n::tr_label(tip)),
1 => Some(if row.fold == Some(true) { crate::i18n::tr("ui.collapse") } else { crate::i18n::tr("ui.expand") }),
@@ -441,7 +444,7 @@ pub(crate) fn tree_header(ui: &mut egui::Ui, title: &str, lens: &mut usize, labe
let (band, _) = ui.allocate_exact_size(vec2(ui.available_width(), t.form_header_height), Sense::hover());
let search = Rect::from_center_size(pos2(band.right() - t.space_8 - t.space_12, band.center().y), vec2(t.space_24, t.space_24));
let response = super::quiet_icon_button_at(ui, search, "zoom", search_label);
crate::scenario::publish_target("render:tree:search", search);
solidcraft_ui_registry::publish_target("render:tree:search", search);
let lens_width = labels
.iter()
.map(|label| ui.painter().layout_no_wrap((*label).into(), crate::fonts::semibold(t.font_small), t.text).size().x + t.space_16 + t.space_4)
+22 -19
View File
@@ -1,7 +1,7 @@
//! Persistent viewport status content. The caller owns items and dispatches returned actions.
use super::{Button, Container, Surface};
use crate::ui_kit::BackingTooltipExt as _;
use crate::{fonts::RichTextExt as _, scenario, theme::Tokens};
use crate::{fonts::RichTextExt as _, theme::Tokens};
use egui::{Id, Rect, Response, RichText, Ui, vec2};
/// Shape and outline communicate the kind independently of colour.
@@ -175,8 +175,8 @@ fn icon_action(ui: &mut Ui, item: &StatusItem, part: &str, icon: &str, label: &s
crate::icons::paint_icon(ui.painter(), glyph, icon, 1.0);
}
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), label));
scenario::publish_target(&target(item, part), response.rect);
scenario::publish_target(&target(item, &format!("{part}_glyph")), glyph);
solidcraft_ui_registry::publish_target(&target(item, part), response.rect);
solidcraft_ui_registry::publish_target(&target(item, &format!("{part}_glyph")), glyph);
response.backing_on_hover_text(hint)
}
@@ -210,13 +210,13 @@ pub(crate) fn viewport_status_item(ui: &mut Ui, item: &StatusItem, state: &mut S
let (disc_rect, _) = ui.allocate_exact_size(vec2(disc_size, disc_size), egui::Sense::hover());
ui.painter().circle_filled(disc_rect.center(), disc_rect.width() / 2.0, disc);
crate::icons::paint_icon(ui.painter(), disc_rect.shrink(t.space_3), item.kind.icon(), 1.0);
scenario::publish_target(&target(item, "icon"), disc_rect);
solidcraft_ui_registry::publish_target(&target(item, "icon"), disc_rect);
let name = ui.label(RichText::new(&item.name).size(t.font_label).semibold().color(t.text));
scenario::publish_target(&target(item, "name"), name.rect);
solidcraft_ui_registry::publish_target(&target(item, "name"), name.rect);
if !item.setting.is_empty() {
let text = RichText::new(&item.setting).monospace().size(t.font_small).color(t.text_dim);
let response = if item.editable { super::button(ui, text, Button::Ghost) } else { ui.label(text) };
scenario::publish_target(&target(item, "setting"), response.rect);
solidcraft_ui_registry::publish_target(&target(item, "setting"), response.rect);
if item.editable {
state.focus_ids.push(response.id);
}
@@ -256,7 +256,7 @@ pub(crate) fn viewport_status_item(ui: &mut Ui, item: &StatusItem, state: &mut S
if focus.has_focus() {
ui.painter().rect_stroke(response.rect.expand(t.space_3), frame.corner_radius, ui.visuals().selection.stroke, egui::StrokeKind::Outside);
}
scenario::publish_target(&target(item, "item"), response.rect);
solidcraft_ui_registry::publish_target(&target(item, "item"), response.rect);
(response.rect, action)
}
@@ -299,8 +299,8 @@ pub(crate) fn viewport_status_row(
}
});
});
scenario::publish_rect("viewport_status:row", row.response.rect);
scenario::publish_count("viewport_status:count", items.len() as f64);
solidcraft_ui_registry::publish_rect("viewport_status:row", row.response.rect);
solidcraft_ui_registry::publish_count("viewport_status:count", items.len() as f64);
if state.return_pending && state.return_focus.is_none_or(|id| !state.focus_ids.contains(&id)) {
state.return_pending = false;
state.return_focus = None;
@@ -383,7 +383,10 @@ pub(crate) fn viewport_status_summary(
if attention {
let dot = egui::pos2(x + t.space_3, rect.center().y);
ui.painter().circle_filled(dot, t.space_3, t.warning);
scenario::publish_target("viewport_status:summary:attention", Rect::from_center_size(dot, vec2(t.space_6, t.space_6)));
solidcraft_ui_registry::publish_target(
"viewport_status:summary:attention",
Rect::from_center_size(dot, vec2(t.space_6, t.space_6)),
);
x += t.space_12;
}
let chevron = Rect::from_center_size(egui::pos2(x + t.space_12, rect.center().y), vec2(t.space_16, t.space_16));
@@ -397,8 +400,8 @@ pub(crate) fn viewport_status_summary(
&[("name", &latest.name), ("count", &items.len().saturating_sub(1).to_string())],
);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), label.as_str()));
scenario::publish_target("viewport_status:summary", rect);
scenario::publish_target(&target(latest, "summary"), rect);
solidcraft_ui_registry::publish_target("viewport_status:summary", rect);
solidcraft_ui_registry::publish_target(&target(latest, "summary"), rect);
response
},
|ui| {
@@ -414,13 +417,13 @@ pub(crate) fn viewport_status_summary(
};
ui.painter().circle_filled(icon.center(), (icon.width() - t.space_4) / 2.0, disc);
crate::icons::paint_icon(ui.painter(), icon.shrink(t.space_4), item.kind.icon(), 1.0);
scenario::publish_target(&target(item, "icon"), icon);
solidcraft_ui_registry::publish_target(&target(item, "icon"), icon);
let name = ui.label(RichText::new(&item.name).semibold().size(t.font_label));
scenario::publish_target(&target(item, "name"), name.rect);
solidcraft_ui_registry::publish_target(&target(item, "name"), name.rect);
let setting = RichText::new(&item.setting).monospace().size(t.font_small).color(t.text_dim);
if item.editable {
let response = super::button(ui, setting, Button::Ghost);
scenario::publish_target(&target(item, "setting"), response.rect);
solidcraft_ui_registry::publish_target(&target(item, "setting"), response.rect);
state.focus_ids.push(response.id);
if response.clicked() {
actions.push(StatusAction::Edit(item.id.clone()));
@@ -428,7 +431,7 @@ pub(crate) fn viewport_status_summary(
}
} else {
let response = ui.label(setting);
scenario::publish_target(&target(item, "setting"), response.rect);
solidcraft_ui_registry::publish_target(&target(item, "setting"), response.rect);
}
if item.has_key {
let label = crate::i18n::format("ui.viewport_status_key", &[("name", &item.name)]);
@@ -481,8 +484,8 @@ pub(crate) fn viewport_status_summary(
}
});
});
scenario::publish_rect("viewport_status:row", row.response.rect);
scenario::publish_count("viewport_status:count", items.len() as f64);
solidcraft_ui_registry::publish_rect("viewport_status:row", row.response.rect);
solidcraft_ui_registry::publish_count("viewport_status:count", items.len() as f64);
if let Some(id) = state.open_key.clone() {
show_card(ctx, free, row.response.rect.left_bottom(), row.response.rect, &id, state, &mut key_contents);
} else {
@@ -519,7 +522,7 @@ fn show_card(
card.show_frame(ui, frame, free, |ui| contents(ui, id));
},
);
scenario::publish_target("viewport_status:card", result.response.rect);
solidcraft_ui_registry::publish_target("viewport_status:card", result.response.rect);
if state.opened_frame != Some(ctx.cumulative_frame_nr())
&& !popup_was_open
&& !egui::Popup::is_any_open(ctx)
+20
View File
@@ -0,0 +1,20 @@
//! Wheel input for the kit's scrolled strips. Camera input never reads the wheel here: it goes
//! through the app's navigation handler.
use egui::Ui;
/// 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: &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
})
}

Some files were not shown because too many files have changed in this diff Show More