mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 03:01:58 +00:00
Fonts: the kit's type API is ui_kit::type_face, the font data stays with the host (#13421)
* 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 * 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 * 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 * 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 * 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 * 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 * 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 * UI kit: layout mode and card geometry in the kit, input shield set by the host (#13260) LayoutMode (with its per-context publish and current), the card gap and the panic-free clamp move into the kit's containers and panels, and Cards into stages. Cards::new takes the workspace width instead of reading the ribbon's constant. The flag that says a report covers the 3D view is now the kit's (input_blocked, set_input_blocked); the app's CadInput guard sets it, and issue_report's own copy is gone. Serde names are unchanged, so saved preferences load as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * UI kit: continuity rows leave the kit, the navigator figure takes its axes as input (#13261) continuity_row and its explained and tip variants are CAD controls over the kernel's Continuity: they move beside their callers in dialogs_surface, still drawn with the kit's choice_row. The Preferences navigator figure takes its axes (screen direction and depth) as NavigatorAxis input; SolidCraft computes them from its default camera. dashed_line is painter geometry and moves into ui_kit::dash with its test. No kit file uses the engine, the record or the viewport outside tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * UI kit: length fields take unit scaling and length parsing from the host (#13270) The kit's length field converted millimetres to and from the design's units through three prefs helpers. Those are the host's unit rules, so Expressions gains unit_scale, length_number and parse_length_mm, and length_drag reads them from the context. DocExpressions implements them with the same prefs helpers, so the app is unchanged. With nothing installed the field edits plain millimetres. No kit file calls prefs outside tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Colour: the sRGB rule and the analysis palette are a shared crate, solidcraft-color (#13291) geom's srgb module (the one sRGB transfer function every encode and decode goes through) moves whole into solidcraft-color with its tests, and so does AnalysisColors, the analysis palette the theme fills and the renderer reads. render, doc, io, engine, gpu and ui-egui use the crate; there is no re-export at the old paths. The theme's unused material_color wrapper is removed, so no non-test theme code names solidcraft_engine. Values are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Material editor: the Remove buttons are translated (material.editor.remove) The editor's two Remove buttons called tr("Remove"), which looks up an id, so they read "Remove" in every language. They use a new id, material.editor.remove, whose text in each locale is the one that locale already gives the plain Remove button prefs.network_host_remove (33 of 35 catalogs have it; the other two show English, as now). The label-literal guard found it once the file's import named tr directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Shared UI crates: ui-text, the message catalogs' machinery as a shared crate (#13290) bidi, Catalog, Locale, lookup, formatting, labels and language choice move from ui-egui's i18n into solidcraft-ui-text, a Shared crate any front end can use. The host installs its catalogs once; with none installed every id reads as itself. SolidCraft's 35 catalogs, the build script that merges them, and the adapters that read engine types (diagnostic, tr_engine_error, and the given-name and browser-label vocabulary as the LocaleNames trait) stay in the app's i18n. The app installs its catalogs when it starts, when an app is built and in the widget test context. Call sites use the crate's path, with no re-export at the old one; messages, ids and fallbacks are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * i18n tests: the painted-English sweeps install the catalogs themselves (#13290) no_english_is_painted_where_the_language_has_a_translation and the_sweep_reports_english_only_where_a_translation_exists read translations without installing the app's catalogs, so they passed only when another test in the same process had installed them first (run alone, every id read as itself). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Theme: the icon palette leaves Tokens; the icon renderer owns it (#13304) A theme file's icon palette was a field of Tokens, so the token set depended on the icon roles ui-egui's build script generates. It is now the icon renderer's per-thread state (icon_svg::set_theme_palette), set alongside the theme's palette by theme::set_file_palette, and theme_files resolves it with resolve_icon_palette (the same overlay merge as the tokens) and exports it as an argument. Rendered pixels, theme files and exports are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Shared UI crates: the token contract as a shared crate, solidcraft-theme (#13304) Tokens with SolidCraft's light and dark values in code (palettes.rs moves in from assets/styles/ui, with the eight primary strokes it shared with the icon artwork written as icon_strokes), colour vision, TabAccent, contrast checks, apply/visuals and the per-thread and per-context theme state move into crates/theme, class Shared. It holds no data files and does not depend on theme files or icon roles; with nothing set, components draw with the built-in tokens. The theme system stays in ui-egui's theme module: the custom-theme glue (prepared theme, tab targets and their fade, blur intent), publish_tab (was TabAccent::publish, which now stores its result through TabAccent::set_current and set_published), set_file_palette, ViewportTheme. ui-egui's build script no longer generates the primary-stroke constants; an icon test holds the crate's literals equal to palette.toml's. Call sites use solidcraft_theme paths with no re-export (one rename pass). Tokens, theme files and rendered pixels are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Theme: the icon lint and the docs read palettes.rs at crates/theme (#13304) lint_icons.py compared the icon review page with assets/styles/ui/palettes.rs, which moved into solidcraft-theme; it reads crates/theme/src/palettes.rs now (cargo xtask icons passes again), and the READMEs, docs and test comments name the new path. set_custom and scoped_tokens say they set tokens only, so a host with an icon renderer sets its palette too; the field contrast test applies each built-in theme with set_file_palette, as the app does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Theme: format the field contrast test (#13304) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * MCP: the server machinery is a shared crate, solidcraft-mcp-server (#12947) JSON-RPC over stdio, the MCP lifecycle, tool and resource methods, tool definitions with their annotations, the argument check and the text and image results move to crates/mcp-server, which has no engine or GPU dependency and is classed Shared. SolidCraft's tools, instructions, resources and the agent's folders stay in crates/mcp and register into it through the Tools trait; its public API, tools list and replies are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * MCP: the shared server tests what it owns, on its own (#12947) Batches (mixed, notifications only, the 1000 limit), an unasked response, the protocol version fallback, null arguments, enum mismatches, the empty default lists and the list-then-call order Tools::call relies on are tested in solidcraft-mcp-server with a fake Tools; the protocol-only lines move out of SolidCraft's wrapper tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: the architecture table lists the shared crates and the MCP split (#12947) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Control: the token files and Effect are a shared crate, solidcraft-control (#12946) The per-launch token, its files, the discovery file and the client's token lookup move from the engine to crates/control, with no engine dependency. Which app they belong to (name, data folder, environment, UTC format, default port) is an `App` value; SolidCraft's is engine::control_auth::APP. `Effect` moves with them; which requests do what stays in the engine's control_pending. Callers switch paths; files and messages are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Control: the loopback control server is shared; SolidCraft gives it its Protocol (#12946) The token-admitted JSON-lines server (framing and line limit, connection cap, control.running/pending/capabilities, busy and timeout replies, control.subscribe events), ControlRequest and the design watch move to solidcraft-control as `server::Hub`. What a request means (its name in messages, its effect, command id, step count, the batch wait, the capabilities) is the app's `server::Protocol`; SolidCraft's is in apps/solidcraft/src/control_server.rs with its listeners and token files. The wire format is unchanged and the server's tests move with it. Also from review: Effect::when_done and the timeout text take the app's word for its document ("design" for SolidCraft), the Listen docs name no SolidCraft preference, and bench_assistant.rs is formatted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Control: the shared server speaks the app's words and listens on loopback only (#12946) The change event, the revision method, the docs path in the unauthorized reply and the document noun come from the app's Protocol; SolidCraft's replies are byte-identical. The design watch is the DocumentWatch. Hub::listen refuses a listener off loopback, so no app can put the channel on the network; SolidCraft binds 127.0.0.1 as before. control_pending.rs is formatted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Control: ui.screenshot and ui.resize are shared over egui (#12946) The window's screenshots in flight (the frame waits, the AO-settled shot, the hidden-window and chat-turn refusals, the 8 s deadline) move from SolveApp to solidcraft_control::screenshot::Shots, with the PNG encoding; ui.resize and the hidden-window test go to window. SolidCraft passes its AO settle, its issue report's frames and its vfs write, so a second app can serve ui.screenshot with the shared crate alone. The issue report and tests reach the queue as app.shots; replies are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Control: ui.screenshot and ui.resize are their own shared crate, solidcraft-control-egui (#12946) d8d35d454 made egui and image dependencies of solidcraft-control, so the headless engine, the CLI and the MCP bridge compiled egui. The screenshot and window modules move unchanged to solidcraft-control-egui; ui-egui uses them from there. cargo tree finds no egui under engine, mcp or control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Control: the app gives the screenshot reply for no frame (#12946) The shared text named SolidCraft's ui.render. Shots::new takes the app's words; SolidCraft passes its reply unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Control: tests for the screenshot gate, a hidden window and frames nobody waits for (#12946) Shots::issue takes a frame on the third frame and 150 ms after the request, or 500 ms later for a view that keeps moving; a hidden window refuses a request and answers one waiting; with nothing pending the frames that came are dropped. Each fails on a one-line mutation of screenshot.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Window chrome: the title bar and window settings from plain inputs (#12940) window_chrome holds what any Craft app's window needs without SolveApp: viewport(title, app_id, custom_titlebar) with SolidCraft's window settings, TitleBar (traffic-light inset, drag area, caption buttons) that returns a WindowAction for the app to run, and the resize zones and hit test. titlebar.rs is SolidCraft's thin use of it: Close asks about unsaved designs first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Window chrome: the macOS menu bar from the kit's menu items (#12940) window_chrome::native_menu builds the menu bar from ui_kit::Item trees (the app menu's system layout, the system's Window entries) and returns the chosen entry as (id, params); the app-menu title comes from the app. SolidCraft's native_menu.rs builds its Items from FileAction with command ids or ui.* ids and maps the ids back, with the same entries, shortcuts and labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Window chrome: the application bar is laid out by TitleBar (#12940) app_bar drew its own drag area, traffic-light inset and caption buttons beside the shared TitleBar; it now runs TitleBar::show and answers the returned WindowAction through titlebar::run, so the title bar tests pin what SolidCraft draws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Window chrome: the menu bar's layout is pure and tested on every system (#12940) MenuTable lays out the whole bar (the app menu's system entries, the Window menu's extras, submenus) and replaces a submenu's entries itself; NativeMenu only turns rows into AppKit items. Nothing is dead code off macOS, and the layout and replace tests run everywhere. The Window menu's id is menu.window; menu ids are never dispatched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Native menu: entries use the control channel's ids, tested on every system (#12940) Quit is app.quit, Settings and Keyboard Shortcuts are ui.prefs {open}, Agent Chat is ui.agent_chat and Help's items are ui.help {item}; the menus are menu.file, menu.file.recent, menu.edit, menu.window and menu.help. The id round trip and the shortcut table are tested without a window on every system; the module builds there for its tests only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Native menu: its control ids do the same over the control channel (#12940) Agent Chat sends ui.agent_chat {toggle}, which the control channel now takes (show or hide as the panel's button does). ui.help runs every item the Help menu sends (docs, messages and install_cli too) and answers an error for an unknown item instead of a silent ok. A test runs the menu's control ids through the control channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Window chrome: a menu of the bar that is off stays off; replace is for submenus (#12940) The bar's own menus take their item's enabled flag. replace() refuses a menu of the bar (it keeps the system entries the layout gave it), now documented and tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Native menu: the actions with a fixed id are a slice, not a sized array (#12940) The list grows with the menu, so it follows the repo's rule for growing lists (cargo xtask lists). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Navigation bar: the bar is a component that returns the slot or menu row chosen (#12944) nav_bar draws the bar, More, its context menu and the open menu from plain data (SlotView, Menu, BarLabels) and returns a NavAction; it keeps which menu is open and reads nothing of the app. navigation_bar_view now describes SolidCraft's slots and its Visual style and View menus as that data and runs the action it gets back with the same calls as before. The Preferences editor keeps using paint_row, which now paints through the component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * View compass: the signed-axis compass is a component that returns the view chosen (#12944) view_compass draws the six signed axes from the camera's view axes (plain right, up and back vectors), the Home chip and the hover tip, and returns a ViewAction: Snap to an axis, Home, or Orbit by this frame's drag delta. Its drag tracker (edge warp included) is shared by the cube and the custom body, which keep their own drawing. SolidCraft turns the camera with the same calls as before (animate_turn with the reverse-on-repeat rule, animate_view, orbit_drag). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Navigation bar: a slot's second registry name is published where it is drawn (#12944) SlotView gains an alias the bar publishes at the slot's handle, on the bar and in More alike. SolidCraft gives the View menu's slot its older name `nav:view` there, instead of looking the slot's handle up after the bar has drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Navigation bar: repaint after running the chosen action (#12944) A menu that stays open after a pick (View) shows its rows' new state on the next frame instead of waiting for the next input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Navigation bar: the component's returned actions are tested from plain data (#12944) Driven with no app: a slot and a row of its menu are returned and a closing menu closes; More returns a folded slot and closes, opens a folded menu in its place, and a menu that does not close on a pick stays open; a submenu row is returned by its own id; the bar's context menu returns Customise, Hide and Reset, and a slot's (on the bar or in More) returns Remove with the slot's stored index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View compass: plain [f64; 3] axes in and out, no geometry type (#12944) ViewAxes, Tip and ViewAction::Snap carry plain arrays, with the dot product a local function, so the component needs neither solidcore_record::Vec3 nor the engine's geometry trait. The compass tip painter takes the axis as an array too (it only reads which axis it is, for the colour); SolidCraft converts at its edge (view_axes, the Snap arm), and the Preferences navigator figure is posed by the same ViewAxes::pose and AXES list as the live compass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View compass: the tip reach is defined once, by the compass (#12944) The Preferences navigator figure placed its tips with its own 38.0 * 0.84; it now scales view_compass::TIP_REACH, so the figure and the live compass cannot drift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View compass: SolidCraft runs only the actions the compass can return (#12944) show() returns a tip or Home, never Orbit (a drag comes from orbit()); the docs say so and signed_axis_view no longer carries a dead Orbit arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View compass: the module list in rustfmt's order (#12944) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View compass: one function places a tip, for the compass and its figure (#12944) tip_offset maps a posed screen direction to where its tip is drawn, applying the reach once at f64; tips() and the Preferences navigator figure both call it, so TIP_REACH goes. The figure's tips move by about one f32 ulp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View compass: show() returns only a tip or Home; a drag is an OrbitStep (#12944) ViewAction is now Snap or Home, so a drag cannot be represented as something the compass returns; orbit() returns its motion as an OrbitStep. signed_axis_view's match is exhaustive over what the compass can return, with no catch-all arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Navigation bar: SolidCraft's View slot is `nav:view` on the bar and in More (#12944) A harness test: with the View slot on the bar, `nav:view` is its handle; folded into More in a narrow view it is not shown until More opens, then `nav:view` is the folded row's handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Start page: the page is a component that returns the entry chosen (#12941) start_page::show draws the rail, the title and one body (action tiles, recent files and samples; a tile grid; a review list) from plain data: labels, icon names, host thumbnails, and what each entry stands for. It returns the entry clicked; it holds no app, design or engine type. home.rs is SolidCraft's use of it: it builds the page from the app's state and runs the chosen FileAction. Recent, menu and sample clicks now go through dispatch after the frame instead of running mid-draw. Every published home rect and handle is unchanged (dumped before/after). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW * Start page: tiles and samples take a host Icon (#12941) ui_kit::Icon is the picture a host gives a kit surface: a kit icon name, a texture it made, or its own painter. FileView's action tiles and sample cards draw it, so the start page shows another app's icons. start_page is a public module like view_compass; Body::Start is boxed (it is far larger than the other bodies). Tests: a tile draws the host's texture icon, a recent row's context menu returns the entry picked, a disabled rail entry returns nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Home: start-page entries name a recovery entry or recent file by index (#12941) FileAction::Recovery, OpenRecent, ForgetRecent and ShowInFolder carry an index into the app's lists instead of a cloned entry or path, and the page borrows titles and file names from the app, so drawing the start page no longer clones an entry or a path per frame. dispatch looks the index up once, when the entry is chosen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Start page: a menu entry's icon is a kit name until MenuItem takes an Icon (#12941) Recent rows' context-menu entries are drawn by ui_kit::MenuItem, whose icon is a kit icon name; #13379 gives it a host Icon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: the type API moves to ui_kit::type_face (#13327) The weights (medium, semibold, bold, RichTextExt), the family and fallback names, the script checks and the CJK font loader state move out of fonts.rs into ui_kit::type_face, which holds no font data and no engine reference. fonts.rs keeps the host part: the embedded Plex faces, ui_font over the engine's font lookup, the system font search and the install functions. with_weight_families lets a host that installs no faces draw the kit's weights with egui's default faces. Call sites use the new path; no re-export at the old one. The font-family guard names the new file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fonts: which faces the web shell fetches is the host's choice (#13327) web_script_font_kind and needs_chinese_font decide which on-demand faces SolidCraft's web shell downloads, so they move from the kit's type API back to fonts.rs, with their assertions in a host test. The Arabic fallback's doc link points at solidcraft_ui_text::bidi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: the CJK font loader tests install the catalogs they read (#13327) CjkFontLoader resolves the language through the installed catalogs, so its tests passed only when another test in the process had installed them first (they failed under `-- cjk` alone). Each now installs them itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: a host with no faces of its own calls type_face::install (#13327) The kit's weights draw only once the context binds the weight families. The host contract is now stated on the module and in docs/ui-kit.md: a host with faces binds them in its definitions (with_weight_families binds any it leaves out), a host with none calls type_face::install once. A test draws the semibold name of a kit status header on a bare context after only that call; without it egui has no face for the family and draws no glyphs. Also puts `pub mod type_face` in rustfmt's order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fonts: the Thai, Hebrew and Arabic names are imported only off the web (#13327) Only the desktop's system font search uses them, so the wasm build warned that the imports were unused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fonts: the wasm test build imports the Thai, Hebrew and Arabic names (#13327) The font tests use them on every target, so the import is gated on test or a desktop target, not on the desktop alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: type_face::install is install_weight_families (#13327) The kit's entry point had the same name as the host's private fonts::install, which installs SolidCraft's faces. The new name says what the kit call does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: install_weight_families says it replaces the font definitions (#13327) It sets egui's default definitions. It cannot merge into the context's own: egui keeps the definitions set this frame private until the next one, and reading the current fonts before the first frame panics. The doc and docs/ui-kit.md say so; a host with faces passes its definitions through with_weight_families instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: the bare-context test also checks a context without the binding (#13327) The control half: a bare context that never called install_weight_families has no semibold family and no face that draws the name, which is why the call is the host contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * UI kit: the egui-faces test sets up through install_weight_families (#13327) It inlined the entry point's body; it now calls the call a host makes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -360,7 +360,7 @@ mod web {
|
||||
/// The start page's recent designs were filled from the stored ones.
|
||||
recents_synced: bool,
|
||||
/// On-demand script fonts: fetched only when a CJK language is selected.
|
||||
font: solidcraft_ui_egui::fonts::CjkFontLoader,
|
||||
font: solidcraft_ui_egui::ui_kit::type_face::CjkFontLoader,
|
||||
font_result: FontLoadResult,
|
||||
/// The fetched Japanese font's bytes: kept so a language change can re-order the fallbacks.
|
||||
cjk_bytes: Option<Vec<u8>>,
|
||||
@@ -1059,7 +1059,7 @@ mod web {
|
||||
}
|
||||
};
|
||||
let effective = solidcraft_ui_text::effective_language(&self.app.preferences.language);
|
||||
if !installed && solidcraft_ui_egui::fonts::needs_cjk_font(effective) {
|
||||
if !installed && solidcraft_ui_egui::ui_kit::type_face::needs_cjk_font(effective) {
|
||||
log::warn!("script font unavailable; showing English for this session");
|
||||
}
|
||||
self.font.finished(installed, &mut self.app.preferences.language);
|
||||
@@ -1093,7 +1093,7 @@ mod web {
|
||||
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
|
||||
/// fetch the Japanese font, so the text does not stay tofu (#720).
|
||||
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::ui_kit::type_face::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
self.fetch_font(ctx, "ja");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ pub(crate) fn axis_tip_scaled(painter: &egui::Painter, c: Pos2, name: &str, d: [
|
||||
// On a hovered (white) disc the letter takes the axis colour.
|
||||
let letter = if hovered { col } else { t.on_accent };
|
||||
let label = name.to_uppercase();
|
||||
let galley = painter.layout_no_wrap(label, crate::fonts::bold(solidcraft_theme::Tokens::get().font_compass * scale), letter);
|
||||
let galley = painter.layout_no_wrap(label, crate::ui_kit::type_face::bold(solidcraft_theme::Tokens::get().font_compass * scale), letter);
|
||||
let origin = at - galley.size() / 2.0;
|
||||
let ppp = painter.ctx().pixels_per_point().max(0.5);
|
||||
let origin = pos2((origin.x * ppp).round() / ppp, (origin.y * ppp).round() / ppp);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! so the parsed blocks are kept per conversation item and parsed again only when its text
|
||||
//! changes.
|
||||
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -43,7 +43,7 @@ fn job(ui: &egui::Ui, spans: &[Span], size: f32, color: Color32) -> LayoutJob {
|
||||
let plain = TextFormat { font_id: base.clone(), color, ..Default::default() };
|
||||
match s {
|
||||
Span::Text(x) => j.append(x, 0.0, plain),
|
||||
Span::Bold(x) => j.append(x, 0.0, TextFormat { font_id: crate::fonts::semibold(size), color: t.text, ..plain }),
|
||||
Span::Bold(x) => j.append(x, 0.0, TextFormat { font_id: crate::ui_kit::type_face::semibold(size), color: t.text, ..plain }),
|
||||
Span::Italic(x) => j.append(x, 0.0, TextFormat { italics: true, ..plain }),
|
||||
Span::Code(x) => {
|
||||
j.append(x, 0.0, TextFormat { font_id: FontId::monospace(size - (t.font_button - t.font_body)), background: t.field, ..plain })
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! or in Preferences), resizable, folded to a thin strip with Minimise, closed with ×. Its side,
|
||||
//! width and fold are preferences, so they come back next time.
|
||||
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use crate::ui_kit::{Container, Edge, Surface};
|
||||
use egui::{Align, Align2, Color32, Id, Layout, Rect, RichText, Sense, Stroke, pos2, vec2};
|
||||
use solidcraft_engine::panels::Flow;
|
||||
|
||||
@@ -1397,7 +1397,8 @@ fn browser_collapsed_rail(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if response.backing_on_hover_text(solidcraft_ui_text::tr("ui.browser_expand")).clicked() {
|
||||
app.preferences.timeline_left_browser_collapsed = false;
|
||||
}
|
||||
let galley = ui.painter().layout_no_wrap(solidcraft_ui_text::tr("ui.browser_title").to_owned(), crate::fonts::semibold(t.font_body), t.text);
|
||||
let galley =
|
||||
ui.painter().layout_no_wrap(solidcraft_ui_text::tr("ui.browser_title").to_owned(), crate::ui_kit::type_face::semibold(t.font_body), t.text);
|
||||
let title = Rect::from_min_max(rect.min + vec2(0.0, title_top), rect.max);
|
||||
ui.painter().add(
|
||||
egui::epaint::TextShape::new(title.center() - galley.size() / 2.0, galley, t.text)
|
||||
@@ -1487,7 +1488,7 @@ fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) {
|
||||
let title = solidcraft_ui_text::tr("ui.browser_title").to_string();
|
||||
let kind = if collapsible { egui::Role::Button } else { egui::Role::Label };
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(kind, true, &title));
|
||||
let galley = ui.painter().layout_no_wrap(title, crate::fonts::semibold(t.font_heading), t.text);
|
||||
let galley = ui.painter().layout_no_wrap(title, crate::ui_kit::type_face::semibold(t.font_heading), t.text);
|
||||
ui.painter().galley(pos2(rect.left() + t.space_6, rect.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
let mut right = rect.right() - t.space_6;
|
||||
if collapsible {
|
||||
|
||||
@@ -605,7 +605,7 @@ fn browser_title_uses_a_single_semibold_run() {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(titles.len(), 1);
|
||||
assert_eq!(titles[0].galley.job.sections[0].format.font_id, crate::fonts::semibold(Tokens::get().font_heading));
|
||||
assert_eq!(titles[0].galley.job.sections[0].format.font_id, crate::ui_kit::type_face::semibold(Tokens::get().font_heading));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3458,7 +3458,7 @@ mod workspace_menu_tests {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(titles.len(), 1, "workspace titles must never be drawn twice");
|
||||
assert_eq!(titles[0].galley.job.sections[0].format.font_id, crate::fonts::semibold(14.0));
|
||||
assert_eq!(titles[0].galley.job.sections[0].format.font_id, crate::ui_kit::type_face::semibold(14.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use crate::ui_kit::{choice_row, expr_row, text_row};
|
||||
use egui::{Color32, Pos2, RichText, Stroke, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -1000,8 +1000,11 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
|
||||
_ => solidcraft_ui_text::Locale::for_language(&app.preferences.language).given_name(name),
|
||||
};
|
||||
let label = format!("{shown_name}{}", if *dirty { " •" } else { "" });
|
||||
let text =
|
||||
ui.painter().layout_no_wrap(label, crate::fonts::semibold(solidcraft_theme::Tokens::get().font_button), t.app_bar_text).size().x;
|
||||
let text = ui
|
||||
.painter()
|
||||
.layout_no_wrap(label, crate::ui_kit::type_face::semibold(solidcraft_theme::Tokens::get().font_button), t.app_bar_text)
|
||||
.size()
|
||||
.x;
|
||||
(text + 40.0).clamp(72.0, MAX_FIT)
|
||||
})
|
||||
.collect();
|
||||
@@ -1089,7 +1092,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
|
||||
let name_display = solidcraft_ui_text::Locale::for_language(&app.preferences.language).given_name(name);
|
||||
let label = format!("{name_display}{}", if *dirty { " •" } else { "" });
|
||||
let font = if active {
|
||||
crate::fonts::semibold(solidcraft_theme::Tokens::get().font_button)
|
||||
crate::ui_kit::type_face::semibold(solidcraft_theme::Tokens::get().font_button)
|
||||
} else {
|
||||
FontId::proportional(solidcraft_theme::Tokens::get().font_button)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Typography belongs to Tokens, and named font families belong to fonts.rs.
|
||||
//! Typography belongs to Tokens, and named font families belong to the kit's type API (ui_kit/type_face.rs).
|
||||
//! Like the colour guard, exceptions have a per-file count and an explicit reason.
|
||||
|
||||
use std::path::Path;
|
||||
@@ -84,8 +84,8 @@ impl<'ast> Visit<'ast> for Guard<'_> {
|
||||
if matches!(last, "ribbon_text" | "face_text") && call.args.iter().nth(3).is_some_and(has_number) {
|
||||
self.found.push(format!("{last}(numeric size)"));
|
||||
}
|
||||
if self.file != "fonts.rs" && names.iter().any(|s| s == "FontFamily") && last == "Name" {
|
||||
self.found.push("FontFamily::Name outside fonts.rs".into());
|
||||
if self.file != "ui_kit/type_face.rs" && names.iter().any(|s| s == "FontFamily") && last == "Name" {
|
||||
self.found.push("FontFamily::Name outside ui_kit/type_face.rs".into());
|
||||
}
|
||||
if self.file.starts_with("ui_kit/")
|
||||
&& (matches!(last, "vec2" | "pos2") || (names.iter().any(|s| s == "Margin") && matches!(last, "same" | "symmetric")))
|
||||
@@ -171,7 +171,7 @@ fn guard_catches_multiline_sizes_and_continues_after_test_modules() {
|
||||
12.0
|
||||
);
|
||||
FontId::proportional(14.0 * zoom);
|
||||
crate::fonts::semibold(13.0);
|
||||
crate::ui_kit::type_face::semibold(13.0);
|
||||
FontFamily::Name("private-family".into());
|
||||
ui.spacing_mut().item_spacing.x = 5.0;
|
||||
ui.add_space(7.0);
|
||||
|
||||
+39
-357
@@ -1,19 +1,24 @@
|
||||
//! Bundled IBM Plex interface faces, user overrides, and language-specific system fallbacks
|
||||
//! (CJK, Hangul, Thai, Hebrew).
|
||||
//! The host part of the interface type: the bundled IBM Plex faces, user overrides, and
|
||||
//! language-specific system fallbacks (CJK, Hangul, Thai, Hebrew), installed under the names the
|
||||
//! kit's type API gives (see [`crate::ui_kit::type_face`]).
|
||||
//! The same definitions are used on desktop and web; the web shell adds Japanese and Chinese
|
||||
//! faces on demand.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{FontDefinitions, FontFamily, FontId, RichText};
|
||||
use egui::{FontDefinitions, FontFamily};
|
||||
|
||||
use crate::ui_kit::type_face::{
|
||||
BOLD_FAMILY, CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, MEDIUM_FAMILY, SEMIBOLD_FAMILY, SYMBOL_FALLBACK, UI_FONT, fallback_order,
|
||||
needs_cjk_font, sans_families, semibold,
|
||||
};
|
||||
// The Thai, Hebrew and Arabic faces come only from the desktop's system font search (and the tests).
|
||||
#[cfg(any(test, not(target_arch = "wasm32")))]
|
||||
use crate::ui_kit::type_face::{ARABIC_FALLBACK, HEBREW_FALLBACK, THAI_FALLBACK};
|
||||
|
||||
const PLEX_SANS: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/ibm-plex-sans/IBMPlexSans-Variable.ttf"));
|
||||
const PLEX_MONO: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf"));
|
||||
const WEIGHTS: [(&str, f32); 4] = [("plex-sans", 400.0), ("ui-medium", 500.0), ("ui-semibold", 600.0), ("ui-bold", 700.0)];
|
||||
|
||||
fn sans_families() -> [FontFamily; 4] {
|
||||
[FontFamily::Proportional, medium_family(), semibold_family(), bold_family()]
|
||||
}
|
||||
const WEIGHTS: [(&str, f32); 4] = [("plex-sans", 400.0), (MEDIUM_FAMILY, 500.0), (SEMIBOLD_FAMILY, 600.0), (BOLD_FAMILY, 700.0)];
|
||||
|
||||
fn all_families() -> impl Iterator<Item = FontFamily> {
|
||||
sans_families().into_iter().chain([FontFamily::Monospace])
|
||||
@@ -35,52 +40,6 @@ pub fn base_font_definitions() -> FontDefinitions {
|
||||
with_symbol_fallback(fonts)
|
||||
}
|
||||
|
||||
fn medium_family() -> FontFamily {
|
||||
FontFamily::Name("ui-medium".into())
|
||||
}
|
||||
|
||||
fn semibold_family() -> FontFamily {
|
||||
FontFamily::Name("ui-semibold".into())
|
||||
}
|
||||
|
||||
fn bold_family() -> FontFamily {
|
||||
FontFamily::Name("ui-bold".into())
|
||||
}
|
||||
|
||||
/// Medium (500) interface text, retaining the language fallbacks and user override.
|
||||
pub fn medium(size: f32) -> FontId {
|
||||
FontId::new(size, medium_family())
|
||||
}
|
||||
|
||||
/// Semibold (600) interface text, retaining the language fallbacks and user override.
|
||||
pub fn semibold(size: f32) -> FontId {
|
||||
FontId::new(size, semibold_family())
|
||||
}
|
||||
|
||||
/// Bold (700) interface text, retaining the language fallbacks and user override.
|
||||
pub fn bold(size: f32) -> FontId {
|
||||
FontId::new(size, bold_family())
|
||||
}
|
||||
|
||||
/// Select a real interface weight without changing the text's size or colour.
|
||||
pub trait RichTextExt {
|
||||
fn medium(self) -> Self;
|
||||
fn semibold(self) -> Self;
|
||||
fn bold_face(self) -> Self;
|
||||
}
|
||||
|
||||
impl RichTextExt for RichText {
|
||||
fn medium(self) -> Self {
|
||||
self.family(medium_family())
|
||||
}
|
||||
fn semibold(self) -> Self {
|
||||
self.family(semibold_family())
|
||||
}
|
||||
fn bold_face(self) -> Self {
|
||||
self.family(bold_family())
|
||||
}
|
||||
}
|
||||
|
||||
// Keep pending definitions too: set_fonts takes effect on the next frame, while the web
|
||||
// fetch and preference update can both finish before that frame starts.
|
||||
fn installed_fonts_id() -> egui::Id {
|
||||
@@ -103,18 +62,6 @@ pub fn setup_bundled_fonts(ctx: &egui::Context) {
|
||||
install(ctx, base_font_definitions());
|
||||
}
|
||||
|
||||
/// Name of the CJK fallback entry in [`egui::FontDefinitions`].
|
||||
pub const CJK_FALLBACK: &str = "cjk_fallback";
|
||||
|
||||
/// Name of the Chinese fallback entry in [`egui::FontDefinitions`], after [`CJK_FALLBACK`].
|
||||
/// A Japanese-only CJK font (Meiryo, Hiragino Gothic) lacks simplified characters, so
|
||||
/// Chinese falls through to this one per glyph.
|
||||
pub const CHINESE_FALLBACK: &str = "chinese_fallback";
|
||||
|
||||
/// Name of the chosen interface font in [`egui::FontDefinitions`], first in every sans
|
||||
/// family when one is set (#1290).
|
||||
pub const UI_FONT: &str = "ui_font";
|
||||
|
||||
/// The interface font a preference names (#1290): an installed family, a font file, or the
|
||||
/// sketch text's built-in family. `None` for the default (empty) or a font that can't be read.
|
||||
pub fn ui_font(name: &str) -> Option<egui::FontData> {
|
||||
@@ -141,23 +88,6 @@ pub fn with_ui_font(mut fonts: FontDefinitions, font: egui::FontData) -> FontDef
|
||||
fonts
|
||||
}
|
||||
|
||||
/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], after the CJK ones.
|
||||
pub const HANGUL_FALLBACK: &str = "hangul_fallback";
|
||||
|
||||
/// Name of the Thai fallback entry in [`egui::FontDefinitions`], after the CJK ones (#1902).
|
||||
pub const THAI_FALLBACK: &str = "thai_fallback";
|
||||
|
||||
/// Name of the Hebrew fallback entry in [`egui::FontDefinitions`], last (#3457).
|
||||
pub const HEBREW_FALLBACK: &str = "hebrew_fallback";
|
||||
|
||||
/// Name of the Arabic fallback entry in [`egui::FontDefinitions`]: first of the fallbacks for an
|
||||
/// Arabic UI, last otherwise. It stays after the Latin interface font, which keeps drawing spaces,
|
||||
/// digits and Latin text, so each Arabic word is one shaped run (see [`crate::bidi`]).
|
||||
pub const ARABIC_FALLBACK: &str = "arabic_fallback";
|
||||
|
||||
/// egui's built-in monospace face, also the last fallback of every sans family (#2955).
|
||||
pub const SYMBOL_FALLBACK: &str = "Hack";
|
||||
|
||||
/// `fonts` with egui's built-in Hack last in every sans family. Ubuntu-Light, NotoEmoji and
|
||||
/// emoji-icon-font lack common symbols such as ▲ ▼ ↑ ↓ → ⁻, which Hack has, so they no longer
|
||||
/// show as empty boxes where no system font fills in (#2955). Text the other faces draw keeps them.
|
||||
@@ -172,62 +102,6 @@ pub fn with_symbol_fallback(mut fonts: egui::FontDefinitions) -> egui::FontDefin
|
||||
fonts
|
||||
}
|
||||
|
||||
/// UI languages whose text needs a CJK script font on the web.
|
||||
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "zh-TW", "ko"];
|
||||
|
||||
/// Whether the UI language needs a CJK font (the web app fetches one only then).
|
||||
pub fn needs_cjk_font(language: &str) -> bool {
|
||||
CJK_LANGUAGES.contains(&language)
|
||||
}
|
||||
|
||||
/// Whether the UI language needs the Simplified or Traditional Chinese web font (#8894).
|
||||
pub fn needs_chinese_font(language: &str) -> bool {
|
||||
matches!(language, "zh" | "zh-TW")
|
||||
}
|
||||
|
||||
/// Which on-demand web face a CJK UI language loads: `"zh"`, `"zh-TW"`, or `"ja"` (also for
|
||||
/// Korean until a Hangul face ships). `None` when the language uses only the bundled Latin faces.
|
||||
pub fn web_script_font_kind(language: &str) -> Option<&'static str> {
|
||||
match language {
|
||||
"zh" => Some("zh"),
|
||||
"zh-TW" => Some("zh-TW"),
|
||||
"ja" | "ko" => Some("ja"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `text` has characters only the CJK fallback draws: kana, CJK ideographs and their
|
||||
/// punctuation, and full-width forms. Hangul is left out: the web app's font has none.
|
||||
pub fn has_cjk(text: &str) -> bool {
|
||||
text.chars().any(|c| {
|
||||
matches!(u32::from(c),
|
||||
0x3000..=0x30FF // CJK punctuation, Hiragana, Katakana
|
||||
| 0x31F0..=0x31FF // Katakana phonetic extensions
|
||||
| 0x3400..=0x4DBF // CJK Extension A
|
||||
| 0x4E00..=0x9FFF // CJK Unified Ideographs
|
||||
| 0xF900..=0xFAFF // CJK Compatibility Ideographs
|
||||
| 0xFF00..=0xFFEF // Half-width and full-width forms
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether any text painted so far this frame has CJK characters (see [`has_cjk`]). The web app
|
||||
/// calls it after drawing the UI, until its CJK font is loaded, so a Japanese design name, file
|
||||
/// name or typed text fetches the font whatever the UI language (#720).
|
||||
pub fn painted_cjk(ctx: &egui::Context) -> bool {
|
||||
let mut layers: Vec<egui::LayerId> = ctx.memory(|m| m.layer_ids().collect());
|
||||
layers.push(egui::LayerId::background());
|
||||
ctx.graphics(|g| layers.iter().filter_map(|l| g.get(*l)).any(|list| list.all_entries().any(|c| shape_has_cjk(&c.shape))))
|
||||
}
|
||||
|
||||
fn shape_has_cjk(shape: &egui::Shape) -> bool {
|
||||
match shape {
|
||||
egui::Shape::Text(t) => has_cjk(&t.galley.job.text),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().any(shape_has_cjk),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `fonts` with `bytes` appended as the last fallback of every interface
|
||||
/// family, after the Latin UI font. `None` when the bytes are not a font.
|
||||
pub fn with_cjk_fallback(fonts: egui::FontDefinitions, bytes: Vec<u8>) -> Option<egui::FontDefinitions> {
|
||||
@@ -261,6 +135,22 @@ fn promote_font_to_front(fonts: &mut FontDefinitions, name: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the UI language needs the Simplified or Traditional Chinese web font (#8894).
|
||||
pub fn needs_chinese_font(language: &str) -> bool {
|
||||
matches!(language, "zh" | "zh-TW")
|
||||
}
|
||||
|
||||
/// Which on-demand web face a CJK UI language loads: `"zh"`, `"zh-TW"`, or `"ja"` (also for
|
||||
/// Korean until a Hangul face ships). `None` when the language uses only the bundled Latin faces.
|
||||
pub fn web_script_font_kind(language: &str) -> Option<&'static str> {
|
||||
match language {
|
||||
"zh" => Some("zh"),
|
||||
"zh-TW" => Some("zh-TW"),
|
||||
"ja" | "ko" => Some("ja"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the downloaded Japanese font with the CJK UI's shared mixed-script baseline.
|
||||
pub fn install_cjk_font_for(ctx: &egui::Context, bytes: Vec<u8>, language: &str) -> bool {
|
||||
install_web_script_fonts(ctx, Some(bytes), None, language)
|
||||
@@ -364,102 +254,6 @@ pub fn is_chinese_font_name(file_name: &str) -> bool {
|
||||
CHINESE_FONT_KEYWORDS.iter().any(|k| name.contains(k))
|
||||
}
|
||||
|
||||
/// Session state of the web app's on-demand script fonts. Pure state so it can be tested off the
|
||||
/// web: the shell does the fetch when [`CjkFontLoader::tick`] asks for it.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CjkFontLoader {
|
||||
status: CjkFontStatus,
|
||||
/// The saved language while this session falls back to English after a failed load.
|
||||
saved_language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum CjkFontStatus {
|
||||
#[default]
|
||||
Unloaded,
|
||||
Loading,
|
||||
Installed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl CjkFontLoader {
|
||||
pub fn status(&self) -> CjkFontStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
/// Called every frame with the live language (a `system` preference resolves to its catalog
|
||||
/// via [`solidcraft_ui_text::effective_language`]). `ready` is true when the shell already has the
|
||||
/// on-demand face that language needs. Returns true when a fetch should start now. After a
|
||||
/// failed load a CJK language falls back to English for this session; the saved preference is
|
||||
/// kept (see [`Self::persisted_language`]). Switching to another CJK language that still needs
|
||||
/// a face starts another fetch (#8894).
|
||||
pub fn tick(&mut self, language: &mut String, ready: bool) -> bool {
|
||||
if self.saved_language.is_some() && language != "en" {
|
||||
// The user picked a language themselves since the fallback: that is the preference now.
|
||||
self.saved_language = None;
|
||||
if self.status == CjkFontStatus::Failed {
|
||||
self.status = CjkFontStatus::Unloaded;
|
||||
}
|
||||
}
|
||||
if !needs_cjk_font(solidcraft_ui_text::effective_language(language)) {
|
||||
return false;
|
||||
}
|
||||
if ready {
|
||||
self.status = CjkFontStatus::Installed;
|
||||
return false;
|
||||
}
|
||||
match self.status {
|
||||
CjkFontStatus::Unloaded | CjkFontStatus::Installed => {
|
||||
self.status = CjkFontStatus::Loading;
|
||||
true
|
||||
}
|
||||
CjkFontStatus::Failed => {
|
||||
self.fall_back(language);
|
||||
false
|
||||
}
|
||||
CjkFontStatus::Loading => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// CJK text is on screen (whatever the UI language). Returns true when the Japanese font fetch
|
||||
/// should start now: once, and never again after a failed load (the UI language stays as it is).
|
||||
pub fn text_needs_font(&mut self) -> bool {
|
||||
if self.status == CjkFontStatus::Unloaded {
|
||||
self.status = CjkFontStatus::Loading;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the session still watches painted text for CJK characters.
|
||||
pub fn watching_text(&self) -> bool {
|
||||
self.status == CjkFontStatus::Unloaded
|
||||
}
|
||||
|
||||
/// Record the outcome of the fetch and install. A `system` preference resolves to its
|
||||
/// catalog first ([`Self::tick`]).
|
||||
pub fn finished(&mut self, installed: bool, language: &mut String) {
|
||||
if installed {
|
||||
self.status = CjkFontStatus::Installed;
|
||||
} else {
|
||||
self.status = CjkFontStatus::Failed;
|
||||
if needs_cjk_font(solidcraft_ui_text::effective_language(language)) {
|
||||
self.fall_back(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fall_back(&mut self, language: &mut String) {
|
||||
self.saved_language = Some(std::mem::replace(language, "en".to_owned()));
|
||||
}
|
||||
|
||||
/// The language to write to storage: the user's own choice, not this session's fallback.
|
||||
pub fn persisted_language<'a>(&'a self, live: &'a str) -> &'a str {
|
||||
self.saved_language.as_deref().unwrap_or(live)
|
||||
}
|
||||
}
|
||||
|
||||
/// Candidate font paths to look for on different operating systems.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const FONT_CANDIDATES: &[&str] = &[
|
||||
@@ -768,25 +562,6 @@ fn find_system_cjk_font() -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The fallback entries in the order they join each family for a UI language.
|
||||
///
|
||||
/// Chinese goes first for `zh`: a Japanese font (Meiryo, Yu Gothic) lacks the simplified
|
||||
/// characters, so the rest of the word falls through to the Chinese font per glyph — and
|
||||
/// epaint centres each face's own line height in the row, so glyphs from two faces of very
|
||||
/// different height (Meiryo ≈ 2.0 em, Microsoft YaHei ≈ 1.4 em) sit at different heights
|
||||
/// inside one word. One face for the whole word keeps them on one baseline.
|
||||
/// Thai, Hebrew and Arabic come last whatever the language: no other face draws them. Arabic
|
||||
/// goes first for `ar` instead: a CJK font such as Arial Unicode also has Arabic letters, without
|
||||
/// the shaping data that joins them.
|
||||
pub fn fallback_order(language: &str) -> [&'static str; 6] {
|
||||
match language {
|
||||
"zh" | "zh-TW" => [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK, ARABIC_FALLBACK],
|
||||
"ko" => [HANGUL_FALLBACK, CJK_FALLBACK, CHINESE_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK, ARABIC_FALLBACK],
|
||||
"ar" => [ARABIC_FALLBACK, CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK],
|
||||
_ => [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK, ARABIC_FALLBACK],
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure font definitions with system CJK, Chinese, Hangul, Thai and Hebrew fallbacks where available,
|
||||
/// ordered for the UI language (see [`fallback_order`]), after the interface font `ui_font`
|
||||
/// when one is chosen (see [`ui_font`]).
|
||||
@@ -1051,6 +826,8 @@ pub(crate) fn test_context() -> egui::Context {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ui_kit::type_face::{RichTextExt as _, bold, medium};
|
||||
use egui::{FontId, RichText};
|
||||
|
||||
/// `SOLIDCRAFT_FONT_DIRS` wins and may list several folders; `CRAFT_FONTS_DIR` is the fallback.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -1566,41 +1343,6 @@ mod tests {
|
||||
assert_eq!(find_chinese_font_in(&[folder.0.join("missing")]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_font_is_fetched_only_for_a_cjk_language() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
for lang in ["en", "de"] {
|
||||
let mut language = lang.to_owned();
|
||||
assert!(!loader.tick(&mut language, false), "no CJK font fetch for {lang}");
|
||||
}
|
||||
let mut language = "ja".to_owned();
|
||||
assert!(loader.tick(&mut language, false), "Japanese starts the fetch");
|
||||
assert!(!loader.tick(&mut language, false), "the fetch starts once");
|
||||
loader.finished(true, &mut language);
|
||||
assert_eq!(loader.status(), CjkFontStatus::Installed);
|
||||
assert_eq!(language, "ja");
|
||||
assert!(!loader.tick(&mut language, true), "ready face needs no fetch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chinese_ui_fetches_again_after_japanese_is_installed() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
let mut language = "ja".to_owned();
|
||||
assert!(loader.tick(&mut language, false));
|
||||
loader.finished(true, &mut language);
|
||||
language = "zh".to_owned();
|
||||
assert!(loader.tick(&mut language, false), "Chinese needs its own face (#8894)");
|
||||
loader.finished(true, &mut language);
|
||||
assert!(!loader.tick(&mut language, true));
|
||||
assert_eq!(web_script_font_kind("zh"), Some("zh"));
|
||||
assert_eq!(web_script_font_kind("zh-TW"), Some("zh-TW"));
|
||||
assert_eq!(web_script_font_kind("ja"), Some("ja"));
|
||||
assert!(needs_chinese_font("zh"));
|
||||
assert!(!needs_chinese_font("ja"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_chinese_subset_draws_simplified_settings() {
|
||||
let bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/fonts/craft-ui-sc/CraftUI-SC.otf")).to_vec();
|
||||
@@ -1615,74 +1357,14 @@ mod tests {
|
||||
assert!(ctx.fonts_mut(|f| f.has_glyphs(&semibold(14.0), "设置")));
|
||||
}
|
||||
|
||||
/// The web shell fetches the Chinese face for a Chinese UI and the Japanese one for Japanese
|
||||
/// and Korean (#8894).
|
||||
#[test]
|
||||
fn cjk_text_is_recognised() {
|
||||
for cjk in ["テスト", "ひらがな", "部品", "テスト", "Bracket(改)", "Plate 板"] {
|
||||
assert!(has_cjk(cjk), "{cjk} has CJK characters");
|
||||
}
|
||||
for other in ["", "Bracket", "Ø12 × 3 mm", "Größe", "Привет", "한국어", "émoji ✓"] {
|
||||
assert!(!has_cjk(other), "{other} has no CJK characters the fallback draws");
|
||||
}
|
||||
}
|
||||
|
||||
/// An English UI fetches the font once CJK text is painted (#720), and a failed fetch leaves
|
||||
/// the language alone.
|
||||
#[test]
|
||||
fn cjk_text_fetches_the_font_with_an_english_ui() {
|
||||
let mut loader = CjkFontLoader::default();
|
||||
let mut language = "en".to_owned();
|
||||
assert!(!loader.tick(&mut language, false));
|
||||
assert!(loader.watching_text());
|
||||
assert!(loader.text_needs_font(), "CJK text starts the fetch");
|
||||
assert!(!loader.text_needs_font(), "the fetch starts once");
|
||||
assert!(!loader.tick(&mut language, false), "no second fetch from the language");
|
||||
assert!(!loader.watching_text());
|
||||
loader.finished(false, &mut language);
|
||||
assert_eq!(loader.status(), CjkFontStatus::Failed);
|
||||
assert_eq!(language, "en");
|
||||
assert!(!loader.text_needs_font(), "no retry loop after a failure");
|
||||
|
||||
let mut loader = CjkFontLoader::default();
|
||||
assert!(loader.text_needs_font());
|
||||
loader.finished(true, &mut language);
|
||||
assert_eq!(loader.status(), CjkFontStatus::Installed);
|
||||
// Switching to Japanese afterwards needs no second fetch when the face is ready.
|
||||
let mut ja = "ja".to_owned();
|
||||
assert!(!loader.tick(&mut ja, true));
|
||||
}
|
||||
|
||||
/// The painted-text check finds CJK in the frame's labels and nothing else.
|
||||
#[test]
|
||||
fn painted_cjk_sees_labels_in_the_frame() {
|
||||
let ctx = egui::Context::default();
|
||||
let run = |text: &'static str| {
|
||||
let mut seen = false;
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
ui.label(text);
|
||||
seen = painted_cjk(ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
seen
|
||||
};
|
||||
assert!(!run("Untitled"));
|
||||
assert!(run("テスト"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_font_fetch_falls_back_for_the_session_without_changing_the_saved_language() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
let mut language = "ja".to_owned();
|
||||
assert!(loader.tick(&mut language, false));
|
||||
loader.finished(false, &mut language);
|
||||
assert_eq!(language, "en", "this session shows English instead of tofu");
|
||||
assert_eq!(loader.persisted_language(&language), "ja", "the saved preference stays Japanese");
|
||||
assert!(!loader.tick(&mut language, false));
|
||||
assert_eq!(loader.persisted_language(&language), "ja");
|
||||
|
||||
// Choosing a language afterwards makes that the saved preference.
|
||||
language = "de".to_owned();
|
||||
assert!(!loader.tick(&mut language, false));
|
||||
assert_eq!(loader.persisted_language(&language), "de");
|
||||
fn the_web_shell_fetches_the_face_each_cjk_language_needs() {
|
||||
assert_eq!(web_script_font_kind("zh"), Some("zh"));
|
||||
assert_eq!(web_script_font_kind("zh-TW"), Some("zh-TW"));
|
||||
assert_eq!(web_script_font_kind("ja"), Some("ja"));
|
||||
assert!(needs_chinese_font("zh"));
|
||||
assert!(!needs_chinese_font("ja"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ pub(super) fn show(app: &mut SolveApp, draft: &mut Draft, ctx: &egui::Context) -
|
||||
// the header's close button remains available when the redundant closing action is omitted.
|
||||
let actions_width = ctx.fonts_mut(|fonts| {
|
||||
[
|
||||
("Open GitHub issue form", crate::fonts::bold(t.font_label), t.space_16),
|
||||
("Open GitHub issue form", crate::ui_kit::type_face::bold(t.font_label), t.space_16),
|
||||
("Copy report", egui::FontId::proportional(t.font_label), t.space_12),
|
||||
("Close draft", egui::FontId::proportional(t.font_label), t.space_12),
|
||||
]
|
||||
|
||||
@@ -818,7 +818,7 @@ pub struct SolveApp {
|
||||
pub(crate) frame_watch: frame_watch::Watch,
|
||||
/// The other open designs (tabs).
|
||||
pub docs: documents::Documents,
|
||||
/// The language the installed font fallback chain is ordered for (`fonts::fallback_order`);
|
||||
/// The language the installed font fallback chain is ordered for (`ui_kit::type_face::fallback_order`);
|
||||
/// changing `preferences.language` re-installs the fonts on the next frame.
|
||||
applied_fonts_language: String,
|
||||
/// The interface font the fonts were last set up with (`Prefs::ui_font`).
|
||||
|
||||
@@ -139,7 +139,9 @@ pub(crate) fn editor_layout(stage: Rect, prefs: &NavigationBar, label_width: imp
|
||||
pub(crate) fn measured(app: &SolveApp, ctx: &egui::Context, content: Rect) -> BarLayout {
|
||||
let t = Tokens::get();
|
||||
let label_width = |slot: &Slot| {
|
||||
ctx.fonts_mut(|fonts| fonts.layout_no_wrap(slot_text(app, slot).to_owned(), crate::fonts::medium(ui_kit::NAV_TEXT), t.form_ink).size().x)
|
||||
ctx.fonts_mut(|fonts| {
|
||||
fonts.layout_no_wrap(slot_text(app, slot).to_owned(), crate::ui_kit::type_face::medium(ui_kit::NAV_TEXT), t.form_ink).size().x
|
||||
})
|
||||
};
|
||||
let mut bar = measure(content, &app.preferences.navigation_bar, navigation_bar::canvas_caps(app), label_width);
|
||||
let Some(_) = app.universal_panel_frame() else {
|
||||
|
||||
@@ -380,7 +380,7 @@ mod tests {
|
||||
.job
|
||||
.sections
|
||||
.iter()
|
||||
.all(|section| section.format.font_id == crate::fonts::semibold(t.font_button));
|
||||
.all(|section| section.format.font_id == crate::ui_kit::type_face::semibold(t.font_button));
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
@@ -2879,7 +2879,9 @@ fn panel_restore_banner(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let (icon, _) = ui.allocate_exact_size(egui::vec2(t.space_16, t.space_16), egui::Sense::hover());
|
||||
crate::icons::paint_ink(ui.painter(), icon, "status_warning", t.form_error);
|
||||
ui.label(
|
||||
egui::RichText::new(app.tr("ui.panel_layout.restore_failed")).font(crate::fonts::semibold(t.font_label)).color(t.form_error),
|
||||
egui::RichText::new(app.tr("ui.panel_layout.restore_failed"))
|
||||
.font(crate::ui_kit::type_face::semibold(t.font_label))
|
||||
.color(t.form_error),
|
||||
);
|
||||
let retry = ui_kit::button(ui, app.tr("ui.panel_layout.try_again"), ui_kit::Button::Primary);
|
||||
crate::scenario::publish_target("prefs:panel_restore_retry", retry.rect);
|
||||
|
||||
@@ -182,7 +182,10 @@ fn preview(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, width: f32)
|
||||
scenario::publish_target("prefs:navbar:stage", stage);
|
||||
scenario::publish_target("prefs:navbar:preview", stage);
|
||||
let bar = navigation_bar_view::editor_layout(stage, &app.preferences.navigation_bar, |slot| {
|
||||
ui.painter().layout_no_wrap(navigation_bar_view::slot_text(app, slot).to_owned(), crate::fonts::medium(ui_kit::NAV_TEXT), t.form_ink).size().x
|
||||
ui.painter()
|
||||
.layout_no_wrap(navigation_bar_view::slot_text(app, slot).to_owned(), crate::ui_kit::type_face::medium(ui_kit::NAV_TEXT), t.form_ink)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
scenario::publish_rect("prefs:navbar:bar", bar.rect);
|
||||
let mut cells = Vec::new();
|
||||
|
||||
@@ -404,7 +404,8 @@ fn tile(app: &SolveApp, ui: &mut Ui, item: Tile<'_>, view: &LayoutPreferencesVie
|
||||
ui.painter().circle_filled(pos2(text_rect.left() + 3.5, text_rect.center().y), 3.5, t.pref_note);
|
||||
text_rect.min.x += t.space_12;
|
||||
}
|
||||
let font = if selected || matches!(item, Tile::Save) { crate::fonts::semibold(t.font_body) } else { FontId::proportional(t.font_body) };
|
||||
let font =
|
||||
if selected || matches!(item, Tile::Save) { crate::ui_kit::type_face::semibold(t.font_body) } else { FontId::proportional(t.font_body) };
|
||||
let mut job = egui::text::LayoutJob::single_section(name.to_owned(), egui::TextFormat::simple(font, ink));
|
||||
job.wrap.max_width = text_rect.width();
|
||||
job.wrap.max_rows = 1;
|
||||
|
||||
@@ -248,7 +248,7 @@ fn create_row(ui: &mut egui::Ui, label: &str, icons: &[&str], first: bool, add:
|
||||
}
|
||||
ui.add_space(t.space_4);
|
||||
if !label.is_empty() {
|
||||
ui.label(egui::RichText::new(label).font(crate::fonts::semibold(t.font_label)).color(t.form_ink));
|
||||
ui.label(egui::RichText::new(label).font(crate::ui_kit::type_face::semibold(t.font_label)).color(t.form_ink));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -602,9 +602,9 @@ fn general_profile_rows_save_replace_apply_and_show_real_state_in_both_themes()
|
||||
assert!((active.height() - 18.0).abs() < 0.5);
|
||||
let t = solidcraft_theme::Tokens::get();
|
||||
assert!(h.shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Rect(rect) if rect.rect == desk && rect.fill == t.selected_bg && rect.stroke.color == t.form_primary)));
|
||||
assert!(h.shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Text(text) if desk.contains(text.pos) && text.galley.job.text == "Desk" && text.galley.job.sections.iter().all(|section| section.format.color == t.selected_ink && section.format.font_id == crate::fonts::semibold(t.font_label)))));
|
||||
assert!(h.shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Text(text) if desk.contains(text.pos) && text.galley.job.text == "Desk" && text.galley.job.sections.iter().all(|section| section.format.color == t.selected_ink && section.format.font_id == crate::ui_kit::type_face::semibold(t.font_label)))));
|
||||
let body = crate::scenario::rect_of("prefs:page_body").unwrap();
|
||||
assert!(h.shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Text(text) if body.contains(text.pos) && text.galley.job.text == "Appearance" && text.galley.job.sections.iter().all(|section| section.format.color == t.selected_ink && section.format.font_id == crate::fonts::semibold(t.font_caption)))));
|
||||
assert!(h.shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Text(text) if body.contains(text.pos) && text.galley.job.text == "Appearance" && text.galley.job.sections.iter().all(|section| section.format.color == t.selected_ink && section.format.font_id == crate::ui_kit::type_face::semibold(t.font_caption)))));
|
||||
assert!(laptop.contains_rect(crate::scenario::rect_of("prefs:profile:Laptop:apply").unwrap()));
|
||||
h.step(&json!({"click":{"handle":"prefs:profile:Laptop:apply"}})).unwrap();
|
||||
h.frames(3);
|
||||
|
||||
@@ -385,7 +385,7 @@ fn timeline_transport(app: &mut SolveApp, ui: &mut egui::Ui, names: bool) {
|
||||
}
|
||||
let controls_width = crate::ui_kit::history_controls_width(controls.len());
|
||||
let title = solidcraft_ui_text::tr("ui.history_title");
|
||||
let galley = ui.painter().layout_no_wrap(title.to_owned(), crate::fonts::semibold(t.font_heading), t.text);
|
||||
let galley = ui.painter().layout_no_wrap(title.to_owned(), crate::ui_kit::type_face::semibold(t.font_heading), t.text);
|
||||
let text_left = handle.right() + t.space_8;
|
||||
let text_right = controls_right - controls_width - t.space_8;
|
||||
let drag_rect = if text_right - text_left >= galley.size().x {
|
||||
@@ -489,7 +489,7 @@ pub(crate) fn dock_drag(app: &mut SolveApp, ui: &egui::Ui, drag: &egui::Response
|
||||
Dock::Left => "ui.timeline_left_sidebar",
|
||||
Dock::UnderBrowser => "ui.history_below_browser",
|
||||
});
|
||||
painter.text(zone.center(), egui::Align2::CENTER_CENTER, label, crate::fonts::semibold(t.font_heading), t.text);
|
||||
painter.text(zone.center(), egui::Align2::CENTER_CENTER, label, crate::ui_kit::type_face::semibold(t.font_heading), t.text);
|
||||
}
|
||||
}
|
||||
if drag.drag_stopped()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Application bar, the workspace toolbar (tabs and panels) and keyboard shortcuts.
|
||||
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::icons::glyphs::pin_icon;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use crate::ui_kit::{self, Container, Edge, Surface};
|
||||
use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, pos2, vec2};
|
||||
use serde_json::json;
|
||||
@@ -423,7 +423,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
pos2(left + 36.0, r.center().y),
|
||||
Align2::LEFT_CENTER,
|
||||
"SolidCraft",
|
||||
crate::fonts::semibold(t.font_heading),
|
||||
crate::ui_kit::type_face::semibold(t.font_heading),
|
||||
t.app_bar_text,
|
||||
);
|
||||
}
|
||||
@@ -1287,7 +1287,7 @@ fn ribbon_button_galley_weight(painter: &egui::Painter, label: &str, width: f32,
|
||||
let size = solidcraft_theme::Tokens::get().font_body;
|
||||
let mut job = egui::text::LayoutJob::simple(
|
||||
lines.join("\n"),
|
||||
if selected { crate::fonts::semibold(size) } else { FontId::proportional(size) },
|
||||
if selected { crate::ui_kit::type_face::semibold(size) } else { FontId::proportional(size) },
|
||||
color,
|
||||
f32::INFINITY,
|
||||
);
|
||||
@@ -1369,7 +1369,10 @@ fn ribbon_tabs(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect) -> Option<bool>
|
||||
let widths: Vec<_> = tabs
|
||||
.iter()
|
||||
.map(|tab| {
|
||||
ui.painter().layout_no_wrap(ribbon_label(app, tab), crate::fonts::semibold(solidcraft_theme::Tokens::get().font_button), t.text).size().x
|
||||
ui.painter()
|
||||
.layout_no_wrap(ribbon_label(app, tab), crate::ui_kit::type_face::semibold(solidcraft_theme::Tokens::get().font_button), t.text)
|
||||
.size()
|
||||
.x
|
||||
+ 24.0
|
||||
})
|
||||
.collect();
|
||||
@@ -1396,7 +1399,7 @@ fn ribbon_tabs(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect) -> Option<bool>
|
||||
let action_labels = [app.tr_label("Render").to_string(), app.tr_label("Cancel").to_string()];
|
||||
let action_widths = action_labels
|
||||
.each_ref()
|
||||
.map(|label| ui.painter().layout_no_wrap(label.clone(), crate::fonts::semibold(t.font_button), t.text).size().x + 24.0);
|
||||
.map(|label| ui.painter().layout_no_wrap(label.clone(), crate::ui_kit::type_face::semibold(t.font_button), t.text).size().x + 24.0);
|
||||
let action_width = if render { action_widths.iter().sum::<f32>() + 2.0 * t.space_2 } else { 0.0 };
|
||||
let search = Rect::from_min_size(
|
||||
pos2(row.right() - search_width - inset - action_width, row.top() + inset),
|
||||
@@ -2196,7 +2199,7 @@ fn ribbon_contents(app: &mut SolveApp, ui: &mut egui::Ui, shape: RibbonShape) ->
|
||||
let label = if finish { app.tr("ui.ribbon_exit").to_string() } else { ribbon_label(app, panel) };
|
||||
let panel_open = panel_open || hidden_running;
|
||||
let font = if panel_open || hot {
|
||||
crate::fonts::semibold(solidcraft_theme::Tokens::get().font_caption)
|
||||
crate::ui_kit::type_face::semibold(solidcraft_theme::Tokens::get().font_caption)
|
||||
} else {
|
||||
FontId::proportional(solidcraft_theme::Tokens::get().font_caption)
|
||||
};
|
||||
@@ -2579,8 +2582,11 @@ fn ribbon_tooltip(app: &SolveApp, response: egui::Response, command: &solidcraft
|
||||
response.backing_on_hover_ui(|ui| {
|
||||
let t = Tokens::get();
|
||||
let name = app.tr_label(crate::render_workspace::interactive_command_info(app, command).label);
|
||||
let name_width =
|
||||
ui.painter().layout_no_wrap(name.to_string(), crate::fonts::semibold(solidcraft_theme::Tokens::get().font_heading), t.text).size().x;
|
||||
let name_width = ui
|
||||
.painter()
|
||||
.layout_no_wrap(name.to_string(), crate::ui_kit::type_face::semibold(solidcraft_theme::Tokens::get().font_heading), t.text)
|
||||
.size()
|
||||
.x;
|
||||
let key_width = crate::keymap::display_for(app, command.id)
|
||||
.map(|key| {
|
||||
ui.painter().layout_no_wrap(key.to_string(), FontId::monospace(solidcraft_theme::Tokens::get().font_body), t.text_dim).size().x + 24.0
|
||||
@@ -2677,7 +2683,7 @@ fn reveal_tooltip(app: &SolveApp, ui: &egui::Ui, rect: Rect, command: &solidcraf
|
||||
/// the grid: a step below the drop-down's own header (`font_caption`) and written as named, not
|
||||
/// in capitals, so it reads as part of the menu rather than above it.
|
||||
fn group_heading_font() -> FontId {
|
||||
crate::fonts::semibold(Tokens::get().font_micro)
|
||||
crate::ui_kit::type_face::semibold(Tokens::get().font_micro)
|
||||
}
|
||||
|
||||
/// A group heading's text: its name in the interface language, as written.
|
||||
@@ -5701,7 +5707,7 @@ mod ribbon_label_tests {
|
||||
.collect();
|
||||
assert_eq!(text.len(), 1, "ribbon labels must never be drawn twice");
|
||||
let text = text[0];
|
||||
assert_eq!(text.galley.job.sections[0].format.font_id, crate::fonts::semibold(13.0));
|
||||
assert_eq!(text.galley.job.sections[0].format.font_id, crate::ui_kit::type_face::semibold(13.0));
|
||||
assert_eq!(text.galley.job.sections[0].format.color, ink);
|
||||
assert!((text.pos.x * scale - (text.pos.x * scale).round()).abs() < 0.001);
|
||||
assert!((text.pos.y * scale - (text.pos.y * scale).round()).abs() < 0.001);
|
||||
@@ -6290,7 +6296,7 @@ mod persistent_ribbon_tests {
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing {id} label"));
|
||||
let font = &text.galley.job.sections[0].format.font_id;
|
||||
assert_eq!(*font, if selected { crate::fonts::semibold(t.font_body) } else { FontId::proportional(t.font_body) });
|
||||
assert_eq!(*font, if selected { crate::ui_kit::type_face::semibold(t.font_body) } else { FontId::proportional(t.font_body) });
|
||||
assert_eq!(text.galley.job.sections[0].format.color, if selected { t.selected_ink } else { t.text });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Actions elements for dense cards; see `docs/ui-kit.md` for tokens and usage.
|
||||
use super::metrics::*;
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use egui::{Response, RichText, Ui, vec2};
|
||||
use solidcraft_theme::Tokens;
|
||||
|
||||
@@ -404,7 +404,8 @@ impl<'a> HeaderActions<'a> {
|
||||
if label.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let text = ui.painter().layout_no_wrap(solidcraft_ui_text::tr_label(label).to_owned(), crate::fonts::bold(t.font_label), t.form_ink);
|
||||
let text =
|
||||
ui.painter().layout_no_wrap(solidcraft_ui_text::tr_label(label).to_owned(), crate::ui_kit::type_face::bold(t.font_label), t.form_ink);
|
||||
let width = (text.size().x.ceil() + t.space_8 * 2.0).min((x - band.left()).max(0.0));
|
||||
let rect = egui::Rect::from_min_max(
|
||||
egui::pos2(x - width, band.center().y - t.form_button_height / 2.0),
|
||||
@@ -715,7 +716,7 @@ pub(crate) fn icon_action(ui: &mut Ui, label: &str, icon: &str, enabled: bool, d
|
||||
/// Compact stage actions share the icon-label button's frame, typography and hit target.
|
||||
pub(crate) fn icon_label_width(ui: &Ui, label: &str, icon: Option<&str>, primary: bool) -> f32 {
|
||||
let t = Tokens::get();
|
||||
let font = if primary { crate::fonts::semibold(t.font_button) } else { egui::FontId::proportional(t.font_label) };
|
||||
let font = if primary { crate::ui_kit::type_face::semibold(t.font_button) } else { egui::FontId::proportional(t.font_label) };
|
||||
ui.painter().layout_no_wrap(label.into(), font, t.form_ink).size().x.ceil()
|
||||
+ 2.0
|
||||
* if primary {
|
||||
@@ -743,7 +744,7 @@ pub(crate) fn selection_actions(ui: &mut Ui, name: &str, actions: &[super::Stage
|
||||
ui.spacing_mut().interact_size.y = t.control_height - t.space_2;
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = t.space_2;
|
||||
let font = crate::fonts::semibold(t.font_small);
|
||||
let font = crate::ui_kit::type_face::semibold(t.font_small);
|
||||
let width = ui.painter().layout_no_wrap(name.into(), font.clone(), accent.ink).size().x.min(t.form_label_width) + t.space_12;
|
||||
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));
|
||||
@@ -817,7 +818,7 @@ fn icon_label_presentation(ui: &mut Ui, label: &str, icon: Option<&str>, enabled
|
||||
ui,
|
||||
egui::Rect::from_min_max(egui::pos2(left, rect.top()), rect.max),
|
||||
label,
|
||||
if primary { crate::fonts::semibold(t.font_button) } else { egui::FontId::proportional(t.font_label) },
|
||||
if primary { crate::ui_kit::type_face::semibold(t.font_button) } else { egui::FontId::proportional(t.font_label) },
|
||||
ink,
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -175,7 +175,7 @@ fn positioned_shared_buttons_center_text_and_respect_disabled_clicks_in_both_the
|
||||
ui,
|
||||
label,
|
||||
rects[index],
|
||||
RichText::new(label).font(crate::fonts::semibold(Tokens::get().font_button)),
|
||||
RichText::new(label).font(crate::ui_kit::type_face::semibold(Tokens::get().font_button)),
|
||||
variant,
|
||||
);
|
||||
assert_eq!(response.rect, rects[index]);
|
||||
|
||||
@@ -232,7 +232,10 @@ pub(crate) fn assistant_approval<R>(ui: &mut Ui, title: &str, body: impl FnOnce(
|
||||
ui.spacing_mut().item_spacing.x = t.space_6;
|
||||
let (icon, _) = ui.allocate_exact_size(vec2(t.space_16, t.space_16), egui::Sense::hover());
|
||||
crate::icons::paint_ink(ui.painter(), icon, "shield", t.selected_ink);
|
||||
ui.add(egui::Label::new(egui::RichText::new(title).font(crate::fonts::semibold(t.font_label)).color(t.form_ink)).wrap());
|
||||
ui.add(
|
||||
egui::Label::new(egui::RichText::new(title).font(crate::ui_kit::type_face::semibold(t.font_label)).color(t.form_ink))
|
||||
.wrap(),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -375,7 +378,9 @@ pub(crate) fn assistant_empty<R>(ui: &mut Ui, title: &str, body: &str, add: impl
|
||||
let (disc, _) = ui.allocate_exact_size(vec2(diameter, diameter), egui::Sense::hover());
|
||||
ui.painter().circle_filled(disc.center(), diameter / 2.0, t.accent_soft);
|
||||
crate::icons::paint_ink(ui.painter(), disc.shrink(t.space_8), "assistant", t.selected_ink);
|
||||
ui.add(egui::Label::new(egui::RichText::new(title).font(crate::fonts::semibold(t.font_body + t.space_2)).color(t.form_ink)).wrap());
|
||||
ui.add(
|
||||
egui::Label::new(egui::RichText::new(title).font(crate::ui_kit::type_face::semibold(t.font_body + t.space_2)).color(t.form_ink)).wrap(),
|
||||
);
|
||||
ui.add(egui::Label::new(egui::RichText::new(body).size(t.font_label).color(t.form_dim)).wrap());
|
||||
add(ui)
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, option)| {
|
||||
ui.painter().layout_no_wrap((*option).to_owned(), crate::fonts::semibold(font_size), t.form_ink).size().x.ceil()
|
||||
ui.painter().layout_no_wrap((*option).to_owned(), crate::ui_kit::type_face::semibold(font_size), t.form_ink).size().x.ceil()
|
||||
+ t.space_16
|
||||
+ if icons.get(index).is_some() { t.space_14 + t.space_6 + t.space_4 } else { 0.0 }
|
||||
})
|
||||
@@ -79,7 +79,7 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
let segment_width = dimension(ui.available_width()) / options.len().max(1) as f32;
|
||||
let widest = shown
|
||||
.iter()
|
||||
.map(|option| ui.painter().layout_no_wrap((*option).to_owned(), crate::fonts::semibold(font_size), t.form_ink).size().x)
|
||||
.map(|option| ui.painter().layout_no_wrap((*option).to_owned(), crate::ui_kit::type_face::semibold(font_size), t.form_ink).size().x)
|
||||
.fold(0.0, f32::max);
|
||||
// Judged on the whole row, which is what a scrollbar narrows, not on one segment.
|
||||
let need = if intrinsic { total } else { (widest + 2.0 * t.space_8) * options.len() as f32 };
|
||||
@@ -159,7 +159,7 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
if index > 0 {
|
||||
ui.painter().vline(segment.left(), segment.y_range(), t.control_border_stroke(t.control_border));
|
||||
}
|
||||
let font = if on { crate::fonts::semibold(font_size) } else { egui::FontId::proportional(font_size) };
|
||||
let font = if on { crate::ui_kit::type_face::semibold(font_size) } else { egui::FontId::proportional(font_size) };
|
||||
let galley = ui.painter().layout_no_wrap((*text).to_string(), font.clone(), t.form_ink);
|
||||
let icon_width = if icons.get(index).is_some() { t.space_14 + t.space_6 } else { 0.0 };
|
||||
let text_rect = egui::Rect::from_min_size(
|
||||
@@ -351,7 +351,7 @@ pub(crate) fn checkbox_row(ui: &mut egui::Ui, key: &str, label: &str, on: bool)
|
||||
/// Compact bold/italic toggles for text. Tokens: scope font/control tokens.
|
||||
/// The glyphs keep the familiar B/I treatment; accessible names use translated style names.
|
||||
pub(crate) fn text_emphasis(ui: &mut Ui, bold: &mut bool, italic: &mut bool, bold_enabled: bool) {
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
for (key, text, value, id, enabled) in [
|
||||
("Bold", egui::RichText::new("B").semibold(), bold, "sketch_text.bold", bold_enabled),
|
||||
("Italic", egui::RichText::new("I").italics(), italic, "sketch_text.italic", true),
|
||||
@@ -447,7 +447,7 @@ pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) ->
|
||||
}
|
||||
let t = Tokens::get();
|
||||
if super::settings_page_active(ui) {
|
||||
let font = if selected { crate::fonts::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) };
|
||||
let font = if selected { crate::ui_kit::type_face::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) };
|
||||
let width = ui.painter().layout_no_wrap(text.to_owned(), font.clone(), t.form_ink).size().x + t.space_16;
|
||||
let response = ui.add_sized(
|
||||
vec2(width.min(dimension(ui.max_rect().width())), t.space_24 - t.space_2),
|
||||
@@ -463,14 +463,14 @@ pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) ->
|
||||
}
|
||||
if super::layout::in_catalog(ui) {
|
||||
let accent = solidcraft_theme::TabAccent::current(ui.ctx());
|
||||
let width = ui.painter().layout_no_wrap(text.to_string(), crate::fonts::semibold(t.font_small), t.form_ink).size().x + t.space_16;
|
||||
let width = ui.painter().layout_no_wrap(text.to_string(), crate::ui_kit::type_face::semibold(t.font_small), t.form_ink).size().x + t.space_16;
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, t.space_16 + t.space_4), egui::Sense::click());
|
||||
ui.painter().rect_filled(rect, t.radius_xl, if selected { accent.soft } else { t.control_fill });
|
||||
paint_text(
|
||||
ui,
|
||||
rect.shrink2(vec2(t.space_8, 0.0)),
|
||||
text,
|
||||
if selected { crate::fonts::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) },
|
||||
if selected { crate::ui_kit::type_face::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) },
|
||||
if selected { accent.ink } else { t.form_ink },
|
||||
false,
|
||||
);
|
||||
@@ -478,7 +478,7 @@ pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) ->
|
||||
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)
|
||||
let width = (ui.painter().layout_no_wrap(text.to_owned(), crate::ui_kit::type_face::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());
|
||||
solidcraft_ui_registry::publish_target(&format!("dialog:list:{key}"), response.rect);
|
||||
@@ -518,7 +518,7 @@ pub(crate) fn thumbnail_choice(ui: &mut Ui, key: &str, text: &str, state: Thumbn
|
||||
let t = Tokens::get();
|
||||
let thumb_size = vec2(t.control_height + t.space_24, t.control_height + t.space_2);
|
||||
let height = thumb_size.y + t.space_8;
|
||||
let font = crate::fonts::semibold(t.font_small);
|
||||
let font = crate::ui_kit::type_face::semibold(t.font_small);
|
||||
let width = ui.painter().layout_no_wrap(text.to_owned(), font.clone(), t.form_ink).size().x.min(t.form_label_width * 2.0)
|
||||
+ thumb_size.x
|
||||
+ t.space_8
|
||||
|
||||
@@ -113,7 +113,7 @@ impl<'a> Confirm<'a> {
|
||||
vec2(t.form_close_size, t.form_close_size),
|
||||
);
|
||||
let title = egui::Rect::from_min_max(heading.min, egui::pos2(close.left(), heading.bottom()));
|
||||
paint_text(ui, title, self.title, crate::fonts::semibold(t.font_subtitle), t.form_ink, false);
|
||||
paint_text(ui, title, self.title, crate::ui_kit::type_face::semibold(t.font_subtitle), t.form_ink, false);
|
||||
ui.interact(title, ui.scope_id().with("title"), egui::Sense::hover())
|
||||
.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), self.title));
|
||||
let response = super::icon_button_at(ui, close, "close", "Close");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Description-driven details content. No tool, document or command knowledge lives here.
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use egui::{RichText, Ui};
|
||||
use solidcraft_theme::Tokens;
|
||||
|
||||
|
||||
@@ -1152,7 +1152,7 @@ pub(crate) fn selection_field(ui: &mut Ui, text: &str, options: Selection) -> Re
|
||||
ui,
|
||||
text_rect,
|
||||
text,
|
||||
crate::fonts::medium(t.font_label),
|
||||
crate::ui_kit::type_face::medium(t.font_label),
|
||||
match (active, filled) {
|
||||
(true, _) if inverted => t.selected_ink,
|
||||
(true, true) => t.selected_ink,
|
||||
|
||||
@@ -151,14 +151,14 @@ impl FileView {
|
||||
|
||||
pub(crate) fn title(ui: &mut Ui, title: &str) {
|
||||
let t = Tokens::get();
|
||||
let galley = ui.painter().layout_no_wrap(title.to_owned(), crate::fonts::semibold(t.font_start_title), t.form_ink);
|
||||
let galley = ui.painter().layout_no_wrap(title.to_owned(), crate::ui_kit::type_face::semibold(t.font_start_title), t.form_ink);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(dimension(ui.available_width()), galley.size().y), Sense::hover());
|
||||
ui.painter().with_clip_rect(ui.clip_rect().intersect(rect)).galley(rect.min, galley, t.form_ink);
|
||||
}
|
||||
|
||||
pub(crate) fn section_title(ui: &mut Ui, title: &str) {
|
||||
let t = Tokens::get();
|
||||
let galley = ui.painter().layout_no_wrap(title.to_owned(), crate::fonts::semibold(t.font_intro), t.form_ink);
|
||||
let galley = ui.painter().layout_no_wrap(title.to_owned(), crate::ui_kit::type_face::semibold(t.font_intro), t.form_ink);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(dimension(ui.available_width()), galley.size().y), Sense::hover());
|
||||
ui.painter().with_clip_rect(ui.clip_rect().intersect(rect)).galley(rect.min, galley, t.form_ink);
|
||||
}
|
||||
@@ -206,7 +206,7 @@ impl FileView {
|
||||
ui,
|
||||
label_rect,
|
||||
label,
|
||||
if selected { crate::fonts::semibold(t.font_heading) } else { FontId::proportional(t.font_heading) },
|
||||
if selected { crate::ui_kit::type_face::semibold(t.font_heading) } else { FontId::proportional(t.font_heading) },
|
||||
ink,
|
||||
false,
|
||||
);
|
||||
@@ -243,7 +243,7 @@ impl FileView {
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
for (text, font) in [
|
||||
(before, FontId::proportional(t.font_button)),
|
||||
(document, crate::fonts::semibold(t.font_button)),
|
||||
(document, crate::ui_kit::type_face::semibold(t.font_button)),
|
||||
(after, FontId::proportional(t.font_button)),
|
||||
] {
|
||||
job.append(text, 0.0, egui::TextFormat::simple(font, t.file_rail_ink));
|
||||
@@ -295,7 +295,8 @@ impl FileView {
|
||||
icon.into().paint(ui.painter(), icon_rect, if ui.is_enabled() { 1.0 } else { 0.45 });
|
||||
let text_rect = Rect::from_min_max(pos2((icon_rect.right() + t.space_14).min(inner.right()), inner.top()), inner.max);
|
||||
let ink = if primary { t.file_primary_ink } else { t.form_ink };
|
||||
let mut title_job = egui::text::LayoutJob::simple(label.to_owned(), crate::fonts::semibold(t.font_heading), ink, text_rect.width());
|
||||
let mut title_job =
|
||||
egui::text::LayoutJob::simple(label.to_owned(), crate::ui_kit::type_face::semibold(t.font_heading), ink, text_rect.width());
|
||||
title_job.wrap.max_rows = 1;
|
||||
let title = ui.fonts_mut(|fonts| fonts.layout_job(title_job));
|
||||
let mut caption_job = egui::text::LayoutJob::simple(subtitle.to_owned(), FontId::proportional(t.font_label), t.file_dim, text_rect.width());
|
||||
@@ -349,7 +350,7 @@ impl FileView {
|
||||
ui,
|
||||
Rect::from_min_max(content.min, pos2(content.right(), half)),
|
||||
row.title,
|
||||
crate::fonts::medium(t.font_button),
|
||||
crate::ui_kit::type_face::medium(t.font_button),
|
||||
t.form_ink,
|
||||
false,
|
||||
);
|
||||
@@ -374,7 +375,7 @@ impl FileView {
|
||||
ui,
|
||||
Rect::from_min_max(content.min, pos2(left + title_width, content.bottom())),
|
||||
row.title,
|
||||
crate::fonts::medium(t.font_button),
|
||||
crate::ui_kit::type_face::medium(t.font_button),
|
||||
t.form_ink,
|
||||
false,
|
||||
);
|
||||
@@ -515,7 +516,8 @@ fn recovery_panel(
|
||||
|ui| {
|
||||
let width = dimension(ui.available_width());
|
||||
let button_width = |text: &str| {
|
||||
ui.painter().layout_no_wrap(text.to_owned(), crate::fonts::semibold(t.font_heading), t.form_ink).size().x + t.space_14 * 2.0
|
||||
ui.painter().layout_no_wrap(text.to_owned(), crate::ui_kit::type_face::semibold(t.font_heading), t.form_ink).size().x
|
||||
+ t.space_14 * 2.0
|
||||
};
|
||||
let recover_width = button_width(recover);
|
||||
let discard_width = discard.map_or(0.0, button_width);
|
||||
@@ -525,7 +527,7 @@ fn recovery_panel(
|
||||
let text_width = (width - icon_width - if wrapped { 0.0 } else { actions_width + t.space_14 }).max(0.0);
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
for (text, font) in [
|
||||
(message.title, crate::fonts::semibold(t.font_button)),
|
||||
(message.title, crate::ui_kit::type_face::semibold(t.font_button)),
|
||||
(message.before_filename, FontId::proportional(t.font_button)),
|
||||
(message.filename, FontId::monospace(t.font_button)),
|
||||
(message.after_filename, FontId::proportional(t.font_button)),
|
||||
@@ -556,7 +558,7 @@ fn recovery_panel(
|
||||
ui,
|
||||
&format!("{id}:recover"),
|
||||
recover_rect,
|
||||
egui::RichText::new(recover).font(crate::fonts::semibold(t.font_heading)),
|
||||
egui::RichText::new(recover).font(crate::ui_kit::type_face::semibold(t.font_heading)),
|
||||
super::actions::Button::Primary,
|
||||
);
|
||||
let b = discard.map(|label| {
|
||||
@@ -585,7 +587,7 @@ fn grid_geometry(available: f32, min_width: f32, count: usize) -> (usize, f32) {
|
||||
fn sample_height(ui: &Ui, width: f32, sample: &SampleCard<'_>) -> f32 {
|
||||
let t = Tokens::get();
|
||||
let inner_width = (dimension(width) - t.space_10 * 2.0).max(0.0);
|
||||
let title = ui.painter().layout(sample.title.to_owned(), crate::fonts::semibold(t.font_button), t.form_ink, inner_width);
|
||||
let title = ui.painter().layout(sample.title.to_owned(), crate::ui_kit::type_face::semibold(t.font_button), t.form_ink, inner_width);
|
||||
let description = ui.painter().layout(sample.description.to_owned(), FontId::proportional(t.font_body), t.file_dim, inner_width);
|
||||
t.file_sample_image_height + t.space_10 * 2.0 + t.space_8 * 2.0 + title.size().y + description.size().y
|
||||
}
|
||||
@@ -593,7 +595,7 @@ fn sample_height(ui: &Ui, width: f32, sample: &SampleCard<'_>) -> f32 {
|
||||
fn sample_card_with_height(ui: &mut Ui, id: &str, width: f32, sample: &SampleCard<'_>, min_height: f32) -> Response {
|
||||
let t = Tokens::get();
|
||||
let inner_width = (dimension(width).min(dimension(ui.available_width())) - t.space_10 * 2.0).max(0.0);
|
||||
let title = ui.painter().layout(sample.title.to_owned(), crate::fonts::semibold(t.font_button), t.form_ink, inner_width);
|
||||
let title = ui.painter().layout(sample.title.to_owned(), crate::ui_kit::type_face::semibold(t.font_button), t.form_ink, inner_width);
|
||||
let description = ui.painter().layout(sample.description.to_owned(), FontId::proportional(t.font_body), t.file_dim, inner_width);
|
||||
let height = sample_height(ui, width, sample).max(min_height);
|
||||
let (rect, response) = allocate(ui, id, width, height, sample.title);
|
||||
|
||||
@@ -1919,7 +1919,7 @@ const ELEMENTS: &[Element] = &[
|
||||
let t = Tokens::get();
|
||||
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);
|
||||
ui.painter().text(rect.center(), egui::Align2::CENTER_CENTER, "Selected", crate::ui_kit::type_face::semibold(t.font_label), t.selected_ink);
|
||||
solidcraft_ui_registry::publish_target("gallery:persistent_button", rect);
|
||||
}),
|
||||
element!("native_button_focus", "gallery:native_button_focus", Some("gallery:native_button_focus"), |ui, _| {
|
||||
|
||||
@@ -13,7 +13,7 @@ pub(crate) struct StripHeading(std::sync::Arc<egui::Galley>);
|
||||
impl StripHeading {
|
||||
pub fn new(painter: &Painter, title: &str) -> Self {
|
||||
let t = Tokens::get();
|
||||
Self(painter.layout_no_wrap(title.to_owned(), crate::fonts::semibold(t.font_heading), t.text))
|
||||
Self(painter.layout_no_wrap(title.to_owned(), crate::ui_kit::type_face::semibold(t.font_heading), t.text))
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
@@ -156,7 +156,7 @@ impl HistoryChip<'_> {
|
||||
} else {
|
||||
t.text
|
||||
};
|
||||
let font = if self.selected { crate::fonts::semibold(t.font_body) } else { egui::FontId::proportional(t.font_body) };
|
||||
let font = if self.selected { crate::ui_kit::type_face::semibold(t.font_body) } else { egui::FontId::proportional(t.font_body) };
|
||||
let text_rect = Rect::from_min_max(
|
||||
pos2(icon.right() + t.space_6 + 1.0, rect.top()),
|
||||
pos2(rect.right() - if self.error || self.warning { t.space_16 } else { t.space_8 }, rect.bottom()),
|
||||
@@ -407,7 +407,7 @@ pub(crate) fn history_seam(ui: &mut egui::Ui, card: Rect, names: bool) -> Histor
|
||||
tip.popup = tip.popup.anchor(seam.pill).align(egui::RectAlign::RIGHT_START).frame(frame);
|
||||
crate::ui_kit::Container::show_tooltip(response.id, tip, |ui| {
|
||||
let t = Tokens::get();
|
||||
ui.label(egui::RichText::new(label).font(crate::fonts::semibold(t.font_body)).color(t.text));
|
||||
ui.label(egui::RichText::new(label).font(crate::ui_kit::type_face::semibold(t.font_body)).color(t.text));
|
||||
ui.label(egui::RichText::new(solidcraft_ui_text::tr("ui.history_edge_hint")).size(t.font_small).color(t.text_dim));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(crate) fn catalog_footer(ui: &mut Ui, text: &str) -> Response {
|
||||
let icon =
|
||||
egui::Rect::from_center_size(egui::pos2(rect.left() + t.space_6, rect.top() + t.space_8 + t.font_small * 0.5), vec2(t.space_12, t.space_12));
|
||||
ui.painter().circle_stroke(icon.center(), t.space_4, t.control_border_stroke(t.form_dim));
|
||||
ui.painter().text(icon.center(), egui::Align2::CENTER_CENTER, "i", crate::fonts::semibold(t.font_badge), t.form_dim);
|
||||
ui.painter().text(icon.center(), egui::Align2::CENTER_CENTER, "i", crate::ui_kit::type_face::semibold(t.font_badge), t.form_dim);
|
||||
let mut job =
|
||||
egui::text::LayoutJob::simple(text.to_owned(), egui::FontId::proportional(t.font_small), t.form_dim, (rect.width() - t.space_24).max(1.0));
|
||||
job.wrap.max_rows = 1;
|
||||
@@ -471,7 +471,7 @@ pub(crate) fn figure_rows(
|
||||
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, &solidcraft_ui_text::tr_label(title).to_uppercase(), crate::fonts::semibold(t.font_caption), t.form_dim, true);
|
||||
paint_text(ui, rect, &solidcraft_ui_text::tr_label(title).to_uppercase(), crate::ui_kit::type_face::semibold(t.font_caption), t.form_dim, true);
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:heading:{title}"), rect);
|
||||
}
|
||||
|
||||
@@ -579,7 +579,7 @@ fn section_with_badge<R>(
|
||||
let badge_value = count;
|
||||
let badge_width = badge_value.as_ref().map_or(0.0, |value| label_width(ui, value) + t.space_12);
|
||||
let reserved = if badge_value.is_some() { badge_width + t.space_12 } else { 0.0 };
|
||||
let mut format = egui::TextFormat::simple(crate::fonts::semibold(t.font_caption), t.form_dim);
|
||||
let mut format = egui::TextFormat::simple(crate::ui_kit::type_face::semibold(t.font_caption), t.form_dim);
|
||||
format.extra_letter_spacing = t.font_caption * t.font_section_tracking;
|
||||
let title_width = ui
|
||||
.fonts_mut(|fonts| fonts.layout_job(egui::text::LayoutJob::single_section(caption.clone(), format)))
|
||||
@@ -588,7 +588,7 @@ fn section_with_badge<R>(
|
||||
.ceil()
|
||||
.min((text_rect.width() - reserved).max(0.0));
|
||||
let title_rect = egui::Rect::from_min_max(text_rect.min, egui::pos2(text_rect.left() + title_width, text_rect.bottom()));
|
||||
paint_text(ui, title_rect, &caption, crate::fonts::semibold(t.font_caption), t.form_dim, true);
|
||||
paint_text(ui, title_rect, &caption, crate::ui_kit::type_face::semibold(t.font_caption), t.form_dim, true);
|
||||
let mut rule_left = title_rect.right() + t.space_12;
|
||||
let mut rule_right = rect.right();
|
||||
if let Some(value) = badge_value {
|
||||
@@ -662,7 +662,7 @@ pub(crate) fn search_fold<R>(ui: &mut Ui, title: &str, count: usize, add: impl F
|
||||
let badge_value = count.to_string();
|
||||
let badge_width = label_width(ui, &badge_value) + t.space_12;
|
||||
let reserved = badge_width + t.space_12;
|
||||
let mut format = egui::TextFormat::simple(crate::fonts::semibold(t.font_caption), t.form_dim);
|
||||
let mut format = egui::TextFormat::simple(crate::ui_kit::type_face::semibold(t.font_caption), t.form_dim);
|
||||
format.extra_letter_spacing = t.font_caption * t.font_section_tracking;
|
||||
let title_width = ui
|
||||
.fonts_mut(|fonts| fonts.layout_job(egui::text::LayoutJob::single_section(caption.clone(), format)))
|
||||
@@ -671,7 +671,7 @@ pub(crate) fn search_fold<R>(ui: &mut Ui, title: &str, count: usize, add: impl F
|
||||
.ceil()
|
||||
.min((text_rect.width() - reserved).max(0.0));
|
||||
let title_rect = egui::Rect::from_min_max(text_rect.min, egui::pos2(text_rect.left() + title_width, text_rect.bottom()));
|
||||
paint_text(ui, title_rect, &caption, crate::fonts::semibold(t.font_caption), t.form_dim, true);
|
||||
paint_text(ui, title_rect, &caption, crate::ui_kit::type_face::semibold(t.font_caption), t.form_dim, true);
|
||||
let mut rule_left = title_rect.right() + t.space_12;
|
||||
let width = badge_width.min((rect.right() - rule_left).max(0.0));
|
||||
let badge = egui::Rect::from_min_size(egui::pos2(rule_left.min(rect.right()), rect.center().y - t.space_10), vec2(width, t.space_10 * 2.0));
|
||||
@@ -837,10 +837,10 @@ fn text_impl(ui: &mut Ui, text: &str, style: Text<'_>, hover_hint: Option<&str>)
|
||||
_ => t.form_ink,
|
||||
};
|
||||
let font = match style {
|
||||
Text::Inline if in_catalog(ui) => crate::fonts::semibold(t.font_small),
|
||||
Text::NameCell(true) | Text::SelectedNameCell => crate::fonts::semibold(t.font_label),
|
||||
Text::NameCell(false) => crate::fonts::medium(t.font_label),
|
||||
Text::Heading(_) => crate::fonts::semibold(t.font_button),
|
||||
Text::Inline if in_catalog(ui) => crate::ui_kit::type_face::semibold(t.font_small),
|
||||
Text::NameCell(true) | Text::SelectedNameCell => crate::ui_kit::type_face::semibold(t.font_label),
|
||||
Text::NameCell(false) => crate::ui_kit::type_face::medium(t.font_label),
|
||||
Text::Heading(_) => crate::ui_kit::type_face::semibold(t.font_button),
|
||||
Text::MonoCell | Text::Value(_, _) => egui::FontId::monospace(t.font_body),
|
||||
Text::MonoNote(_) => egui::FontId::monospace(t.font_small),
|
||||
Text::Hint(_) | Text::Error(_) => egui::FontId::proportional(t.font_body),
|
||||
@@ -926,7 +926,7 @@ pub(crate) fn summary_row(ui: &mut Ui, name: &str, summary: &str, enabled: &mut
|
||||
);
|
||||
let title = egui::Rect::from_min_max(text.min, egui::pos2(text.right(), text.center().y));
|
||||
let detail = egui::Rect::from_min_max(egui::pos2(text.left(), text.center().y), text.max);
|
||||
paint_text(ui, title, name, crate::fonts::semibold(t.font_label), t.form_ink, false);
|
||||
paint_text(ui, title, name, crate::ui_kit::type_face::semibold(t.font_label), t.form_ink, false);
|
||||
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}")));
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn look_chain(ui: &mut Ui, id: &str, labels: [&str; 4], looks: [Option<Spher
|
||||
if step == active {
|
||||
painter.circle_stroke(center, radius + ring_width, Stroke::new(ring_width, t.render_accent));
|
||||
}
|
||||
let font = if step == active { crate::fonts::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) };
|
||||
let font = if step == active { crate::ui_kit::type_face::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) };
|
||||
let label_width = ui.painter().layout_no_wrap(label.to_owned(), font.clone(), t.form_ink).size().x;
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Menus share one popup surface and one row painter. Command dispatch stays with callers.
|
||||
use crate::ui_kit::{Container, Surface};
|
||||
use crate::{fonts::RichTextExt as _, icons};
|
||||
use crate::{icons, ui_kit::type_face::RichTextExt as _};
|
||||
use egui::{Color32, FontId, Rect, Response, Sense, Stroke, Ui, pos2, vec2};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
@@ -307,7 +307,7 @@ impl Menu {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), t.space_24), Sense::hover());
|
||||
let mut job = egui::text::LayoutJob::simple_singleline(
|
||||
solidcraft_ui_text::tr_label(label).to_uppercase(),
|
||||
crate::fonts::semibold(t.font_small),
|
||||
crate::ui_kit::type_face::semibold(t.font_small),
|
||||
t.form_dim,
|
||||
);
|
||||
for section in &mut job.sections {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl ContentWidth {
|
||||
/// Width of a strong configuration name, using the same font as its table cell.
|
||||
pub(crate) fn name(ui: &Ui, text: &str) -> f32 {
|
||||
let t = Tokens::get();
|
||||
ui.painter().layout_no_wrap(text.to_owned(), crate::fonts::semibold(t.font_label), t.form_ink).size().x.ceil()
|
||||
ui.painter().layout_no_wrap(text.to_owned(), crate::ui_kit::type_face::semibold(t.font_label), t.form_ink).size().x.ceil()
|
||||
}
|
||||
|
||||
pub(crate) fn button(ui: &Ui, label: &str) -> f32 {
|
||||
|
||||
@@ -57,6 +57,7 @@ mod text;
|
||||
mod text_menu;
|
||||
mod theme_picker;
|
||||
mod tree;
|
||||
pub mod type_face;
|
||||
mod value_focus;
|
||||
mod viewport_status;
|
||||
mod wheel;
|
||||
|
||||
@@ -192,7 +192,7 @@ pub(crate) fn navigation_button_editable(ui: &mut Ui, rect: Rect, button: Naviga
|
||||
);
|
||||
}
|
||||
let ink = navigation_ink(&t, fill);
|
||||
let text = button.labels.then(|| ui.painter().layout_no_wrap(button.label.to_owned(), crate::fonts::medium(NAV_TEXT), ink));
|
||||
let text = button.labels.then(|| ui.painter().layout_no_wrap(button.label.to_owned(), crate::ui_kit::type_face::medium(NAV_TEXT), ink));
|
||||
let mut x = if button.menu {
|
||||
rect.left() + NAV_MENU_LEFT + if editing { NAV_BORDER } else { 0.0 }
|
||||
} else {
|
||||
|
||||
@@ -60,7 +60,7 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
Rect::from_center_size(pos2(rect.left() + NAV_MENU_INSET + NAV_MENU_ICON / 2.0, rect.center().y), vec2(NAV_MENU_ICON, NAV_MENU_ICON));
|
||||
icons::paint_ink(ui.painter(), icon, self.icon, if self.selected { ink } else { t.form_dim });
|
||||
}
|
||||
let font = if self.selected { crate::fonts::semibold(NAV_TEXT) } else { FontId::proportional(NAV_TEXT) };
|
||||
let font = if self.selected { crate::ui_kit::type_face::semibold(NAV_TEXT) } else { FontId::proportional(NAV_TEXT) };
|
||||
// The label ends before the detail and the check or chevron: one that doesn't fit is cut
|
||||
// with an ellipsis and shown whole on hover, as a long name in a list is (#8700).
|
||||
let mut end = rect.right() - NAV_MENU_INSET - if self.selected || self.submenu { t.space_14 + t.space_4 } else { 0.0 };
|
||||
@@ -97,7 +97,7 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
pub(crate) fn navigation_menu_heading(ui: &mut Ui, title: &str) {
|
||||
let t = Tokens::get();
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width().max(0.0), NAV_MENU_HEADING), Sense::hover());
|
||||
let mut format = egui::TextFormat::simple(crate::fonts::semibold(NAV_HEADING_TEXT), t.form_dim);
|
||||
let mut format = egui::TextFormat::simple(crate::ui_kit::type_face::semibold(NAV_HEADING_TEXT), t.form_dim);
|
||||
format.extra_letter_spacing = NAV_HEADING_TEXT * NAV_HEADING_TRACKING;
|
||||
let galley = ui.painter().layout_job(egui::text::LayoutJob::single_section(title.to_uppercase(), format));
|
||||
ui.painter().galley(rect.min + vec2(t.space_8, t.space_6), galley, t.form_dim);
|
||||
@@ -141,7 +141,7 @@ pub(crate) fn navigation_tooltip(response: &Response, along: Rect, edge: Navigat
|
||||
ui.spacing_mut().item_spacing.x = t.space_8;
|
||||
ui.spacing_mut().interact_size.y = 0.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(label).font(crate::fonts::medium(t.font_body)).color(t.nav_tip_ink));
|
||||
ui.label(egui::RichText::new(label).font(crate::ui_kit::type_face::medium(t.font_body)).color(t.nav_tip_ink));
|
||||
if let Some(key) = shortcut {
|
||||
egui::Frame::NONE
|
||||
.stroke(egui::Stroke::new(NAV_BORDER, t.nav_tip_key_border))
|
||||
|
||||
@@ -107,7 +107,14 @@ fn header_actions(
|
||||
};
|
||||
let left = (icon_rect.right() + t.space_8).min(right);
|
||||
let title_rect = egui::Rect::from_min_max(egui::pos2(left, rect.top()), egui::pos2(right.max(left), rect.bottom()));
|
||||
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);
|
||||
paint_text(
|
||||
ui,
|
||||
title_rect,
|
||||
title,
|
||||
crate::ui_kit::type_face::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));
|
||||
solidcraft_ui_registry::publish_rect("dialog:header", rect);
|
||||
@@ -218,9 +225,9 @@ impl<'a> TopHeader<'a> {
|
||||
} else {
|
||||
crate::icons::paint_icon(ui.painter(), self.icon, icon, 1.0);
|
||||
}
|
||||
let heading = crate::fonts::semibold(t.font_heading);
|
||||
let heading = crate::ui_kit::type_face::semibold(t.font_heading);
|
||||
match state.text.as_deref() {
|
||||
Some(status) => paint_text(ui, self.title, status, crate::fonts::medium(t.font_label), t.form_dim, false),
|
||||
Some(status) => paint_text(ui, self.title, status, crate::ui_kit::type_face::medium(t.font_label), t.form_dim, false),
|
||||
None => paint_text(ui, self.title, title, heading.clone(), t.form_ink, false),
|
||||
}
|
||||
let title_response = ui.interact(self.title, ui.make_persistent_id("title_text"), egui::Sense::hover());
|
||||
@@ -266,7 +273,8 @@ pub(crate) fn inspector_header(ui: &mut Ui, icon: &str, title: &str, kind: &str)
|
||||
}
|
||||
});
|
||||
}
|
||||
let badge_width = ui.painter().layout_no_wrap(kind.to_owned(), crate::fonts::semibold(t.font_small), t.form_ink).size().x + t.space_16;
|
||||
let badge_width =
|
||||
ui.painter().layout_no_wrap(kind.to_owned(), crate::ui_kit::type_face::semibold(t.font_small), t.form_ink).size().x + t.space_16;
|
||||
let badge = egui::Rect::from_center_size(
|
||||
egui::pos2(more_rect.left() - t.space_8 - badge_width / 2.0, rect.center().y),
|
||||
vec2(badge_width, t.space_16 + t.space_4),
|
||||
@@ -276,7 +284,7 @@ pub(crate) fn inspector_header(ui: &mut Ui, icon: &str, title: &str, kind: &str)
|
||||
super::workspace_chip(&mut badge_ui, kind);
|
||||
}
|
||||
let name = egui::Rect::from_min_max(egui::pos2(icon_rect.right() + t.space_8, rect.top()), egui::pos2(badge.left() - t.space_8, rect.bottom()));
|
||||
paint_text(ui, name, title, crate::fonts::semibold(t.font_label), t.form_ink, false);
|
||||
paint_text(ui, name, title, crate::ui_kit::type_face::semibold(t.font_label), t.form_ink, false);
|
||||
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);
|
||||
@@ -1362,7 +1370,14 @@ fn page_list_sized<S>(
|
||||
ui.add_space(t.space_8);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(dimension(ui.available_width()), t.space_24), egui::Sense::hover());
|
||||
let rect = rect.shrink2(vec2(t.space_10, 0.0));
|
||||
paint_text(ui, rect, &solidcraft_ui_text::tr(group).to_uppercase(), crate::fonts::semibold(t.font_small), t.form_dim, true);
|
||||
paint_text(
|
||||
ui,
|
||||
rect,
|
||||
&solidcraft_ui_text::tr(group).to_uppercase(),
|
||||
crate::ui_kit::type_face::semibold(t.font_small),
|
||||
t.form_dim,
|
||||
true,
|
||||
);
|
||||
}
|
||||
let label = solidcraft_ui_text::tr(page.label);
|
||||
let (rect, response) =
|
||||
@@ -1389,7 +1404,7 @@ fn page_list_sized<S>(
|
||||
ui,
|
||||
text_rect,
|
||||
label,
|
||||
if on { crate::fonts::semibold(t.font_label) } else { egui::FontId::proportional(t.font_label) },
|
||||
if on { crate::ui_kit::type_face::semibold(t.font_label) } else { egui::FontId::proportional(t.font_label) },
|
||||
ink,
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -185,9 +185,9 @@ pub(crate) fn panel_host_control_label_opacity(
|
||||
if !chrome.icon_only && !chrome.grip_only {
|
||||
let ink = ink.gamma_multiply(label_opacity);
|
||||
let font = if chrome.title {
|
||||
crate::fonts::semibold(t.font_heading)
|
||||
crate::ui_kit::type_face::semibold(t.font_heading)
|
||||
} else if chrome.selected {
|
||||
crate::fonts::semibold(t.font_label)
|
||||
crate::ui_kit::type_face::semibold(t.font_label)
|
||||
} else {
|
||||
egui::FontId::proportional(t.font_label)
|
||||
};
|
||||
|
||||
@@ -712,7 +712,7 @@ fn universal_header_reuses_title_font_and_six_dot_grip_without_idle_control_tile
|
||||
for clipped in &output.shapes {
|
||||
crate::test_support::each_shape(&clipped.shape, &mut |shape| match shape {
|
||||
egui::Shape::Text(text) if text.galley.job.text == "Shared title" => {
|
||||
assert_eq!(text.galley.job.sections.first().unwrap().format.font_id, crate::fonts::semibold(t.font_heading));
|
||||
assert_eq!(text.galley.job.sections.first().unwrap().format.font_id, crate::ui_kit::type_face::semibold(t.font_heading));
|
||||
titles += 1;
|
||||
}
|
||||
egui::Shape::Text(text) if text.galley.job.text == "Shared tab" => {
|
||||
@@ -754,9 +754,9 @@ fn universal_drag_source_changes_only_label_ink_and_keeps_existing_fonts() {
|
||||
t.form_ink
|
||||
};
|
||||
let font = if title {
|
||||
crate::fonts::semibold(t.font_heading)
|
||||
crate::ui_kit::type_face::semibold(t.font_heading)
|
||||
} else if selected {
|
||||
crate::fonts::semibold(t.font_label)
|
||||
crate::ui_kit::type_face::semibold(t.font_label)
|
||||
} else {
|
||||
egui::FontId::proportional(t.font_label)
|
||||
};
|
||||
@@ -1358,12 +1358,12 @@ fn heading_weight_field_surfaces_and_disclosures_follow_tokens_in_both_themes()
|
||||
assert_eq!(text.galley.job.sections.first().unwrap().format.font_id.family, egui::FontFamily::Monospace);
|
||||
}
|
||||
if text.galley.job.text == "Fillet" {
|
||||
assert_eq!(text.galley.job.sections.first().unwrap().format.font_id, crate::fonts::semibold(t.font_heading));
|
||||
assert_eq!(text.galley.job.sections.first().unwrap().format.font_id, crate::ui_kit::type_face::semibold(t.font_heading));
|
||||
*title += 1;
|
||||
}
|
||||
if text.galley.job.text == "OPTIONS" {
|
||||
let format = &text.galley.job.sections.first().unwrap().format;
|
||||
assert_eq!(format.font_id, crate::fonts::semibold(t.font_caption));
|
||||
assert_eq!(format.font_id, crate::ui_kit::type_face::semibold(t.font_caption));
|
||||
assert!(format.extra_letter_spacing > 0.0);
|
||||
*section += 1;
|
||||
}
|
||||
|
||||
@@ -461,11 +461,15 @@ pub(crate) fn settings_heading_with_tag(ui: &mut Ui, title: &str, description: &
|
||||
let (rect, response) =
|
||||
ui.allocate_exact_size(vec2(dimension(ui.available_width()), if page { t.space_24 } else { t.space_16 + t.space_3 }), Sense::hover());
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), title));
|
||||
paint_text(ui, rect, title, crate::fonts::semibold(if page { t.font_page_heading } else { t.font_intro }), t.form_ink, false);
|
||||
paint_text(ui, rect, title, crate::ui_kit::type_face::semibold(if page { t.font_page_heading } else { t.font_intro }), t.form_ink, false);
|
||||
if let Some(tag) = tag {
|
||||
let width = ui
|
||||
.painter()
|
||||
.layout_no_wrap(title.to_owned(), crate::fonts::semibold(if page { t.font_page_heading } else { t.font_intro }), t.form_ink)
|
||||
.layout_no_wrap(
|
||||
title.to_owned(),
|
||||
crate::ui_kit::type_face::semibold(if page { t.font_page_heading } else { t.font_intro }),
|
||||
t.form_ink,
|
||||
)
|
||||
.size()
|
||||
.x;
|
||||
let start = (rect.left() + width + t.space_8).min(rect.right());
|
||||
@@ -618,7 +622,7 @@ pub(crate) fn settings_badge(ui: &mut Ui, text: &str, warning: bool) {
|
||||
let ink =
|
||||
if warning { if solidcraft_theme::contrast_ratio(t.warning, t.dialog_body) >= 4.5 { t.warning } else { t.form_ink } } else { t.form_success };
|
||||
let fill = if warning { t.warning.gamma_multiply(0.1) } else { t.form_success_fill };
|
||||
let galley = ui.painter().layout_no_wrap(text.to_owned(), crate::fonts::semibold(t.font_small), ink);
|
||||
let galley = ui.painter().layout_no_wrap(text.to_owned(), crate::ui_kit::type_face::semibold(t.font_small), ink);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(galley.size().x + t.space_16, t.space_24), Sense::hover());
|
||||
ui.painter().rect_filled(r, t.radius_xl, fill);
|
||||
ui.painter().galley(r.center() - galley.size() / 2.0, galley, ink);
|
||||
@@ -704,7 +708,7 @@ pub(crate) fn option_card(ui: &mut Ui, label: &str, note: Option<&str>, selected
|
||||
ui.painter(),
|
||||
label_rect,
|
||||
label,
|
||||
if selected { crate::fonts::semibold(t.font_label) } else { FontId::proportional(t.font_label) },
|
||||
if selected { crate::ui_kit::type_face::semibold(t.font_label) } else { FontId::proportional(t.font_label) },
|
||||
ink,
|
||||
if matches!(figure, PreferenceFigure::ContactShading(_)) { 2 } else { 1 },
|
||||
);
|
||||
@@ -878,7 +882,7 @@ pub(crate) fn scale_specimen(ui: &mut Ui, scale: f32) {
|
||||
&p,
|
||||
button,
|
||||
solidcraft_ui_text::tr_label("OK"),
|
||||
crate::fonts::semibold(t.font_label * scale),
|
||||
crate::ui_kit::type_face::semibold(t.font_label * scale),
|
||||
if t.borders.bevel.is_some() { t.form_ink } else { t.on_accent },
|
||||
);
|
||||
let feature = Rect::from_min_size(origin + vec2(0.0, line_h + t.space_8 * scale), vec2(r.width() - t.space_10 * 2.0, line_h));
|
||||
@@ -889,7 +893,7 @@ pub(crate) fn scale_specimen(ui: &mut Ui, scale: f32) {
|
||||
pos2(icon.right() + t.space_6 * scale, feature.center().y),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
"Extrude1",
|
||||
crate::fonts::semibold(t.font_label * scale),
|
||||
crate::ui_kit::type_face::semibold(t.font_label * scale),
|
||||
t.selected_ink,
|
||||
);
|
||||
p.text(feature.right_center() - vec2(t.space_8, 0.0), egui::Align2::RIGHT_CENTER, "12.5 mm", font, t.selected_ink);
|
||||
@@ -986,7 +990,7 @@ pub(crate) fn status_palette_sample(ui: &mut Ui, mode: ColorVision) {
|
||||
] {
|
||||
let ink = if solidcraft_theme::contrast_ratio(t.on_accent, fill) >= 4.5 { t.on_accent } else { t.tab_on_fill_dark };
|
||||
let label = solidcraft_ui_text::tr(label);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_owned(), crate::fonts::semibold(t.font_small), ink);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_owned(), crate::ui_kit::type_face::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());
|
||||
solidcraft_ui_registry::publish_rect(&format!("prefs:status_sample:{label}"), r);
|
||||
ui.painter().rect_filled(r, t.radius_xl, fill);
|
||||
|
||||
@@ -72,7 +72,7 @@ fn choice_card(ui: &mut Ui, width: f32, choice: &SettingsChoice<'_>, selected: b
|
||||
let icon_width = if choice.icon.is_some() { t.space_14 + t.space_6 } else { 0.0 };
|
||||
let title_galley = ui.painter().layout(
|
||||
choice.title.to_owned(),
|
||||
crate::fonts::semibold(t.font_label),
|
||||
crate::ui_kit::type_face::semibold(t.font_label),
|
||||
if selected { selected_ink } else { t.form_ink },
|
||||
(width - t.space_24 * 2.0 - icon_width).max(1.0),
|
||||
);
|
||||
@@ -111,7 +111,14 @@ fn choice_card(ui: &mut Ui, width: f32, choice: &SettingsChoice<'_>, selected: b
|
||||
let title = Rect::from_min_max(pos2(radio.x + t.space_12, rect.top()), pos2(rect.right() - t.space_8, rect.bottom()));
|
||||
let summary_width = ui.painter().layout_no_wrap(choice.summary.into(), FontId::proportional(t.font_small), t.form_dim).size().x;
|
||||
let title = Rect::from_min_max(title.min, title.max - vec2(summary_width + t.space_8, 0.0));
|
||||
paint_text(ui, title, choice.title, crate::fonts::semibold(t.font_label), if selected { selected_ink } else { t.form_ink }, false);
|
||||
paint_text(
|
||||
ui,
|
||||
title,
|
||||
choice.title,
|
||||
crate::ui_kit::type_face::semibold(t.font_label),
|
||||
if selected { selected_ink } else { t.form_ink },
|
||||
false,
|
||||
);
|
||||
let body = Rect::from_min_max(pos2(rect.right() - t.space_8 - summary_width, rect.top()), rect.max - vec2(t.space_8, 0.0));
|
||||
paint_text(ui, body, choice.summary, FontId::proportional(t.font_small), t.form_dim, false);
|
||||
} else {
|
||||
|
||||
@@ -155,7 +155,7 @@ fn paint_card(ui: &mut Ui, card: &Card, width: f32, tile: bool) -> Response {
|
||||
CardInk::Accent => t.selected_ink,
|
||||
CardInk::Text => t.form_ink,
|
||||
};
|
||||
let font = if look.bold { crate::fonts::semibold(t.font_label) } else { FontId::proportional(t.font_label) };
|
||||
let font = if look.bold { crate::ui_kit::type_face::semibold(t.font_label) } else { FontId::proportional(t.font_label) };
|
||||
let label_rect =
|
||||
Rect::from_min_size(pos2(rect.left() + t.space_2, figure.bottom() + gap(&t)), vec2((rect.width() - 2.0 * t.space_2).max(0.0), t.space_16));
|
||||
centred_text(p, label_rect, card.label, font, dim(ink));
|
||||
@@ -360,7 +360,7 @@ pub(crate) fn preset_card(
|
||||
icon(p, Rect::from_center_size(box_rect.center(), box_rect.size() * 0.55), dim(box_ink));
|
||||
let text_left = box_rect.right() + t.space_12;
|
||||
let text_width = (rect.right() - t.space_14 - t.space_24 - text_left).max(0.0);
|
||||
let name_font = crate::fonts::semibold(t.font_heading.min(t.font_subtitle));
|
||||
let name_font = crate::ui_kit::type_face::semibold(t.font_heading.min(t.font_subtitle));
|
||||
let name_ink = if selected { t.selected_ink } else { t.form_ink };
|
||||
let name_rect = Rect::from_min_size(pos2(text_left, rect.center().y - t.space_16), vec2(text_width, t.space_16));
|
||||
super::text::paint_text(ui, name_rect, name, name_font, dim(name_ink), false);
|
||||
@@ -425,7 +425,7 @@ pub(crate) fn settings_plugin_card(
|
||||
let version_galley = ui.painter().layout_no_wrap(version.into(), FontId::proportional(t.font_small), t.form_dim);
|
||||
let title = ui.painter().layout(
|
||||
name.into(),
|
||||
crate::fonts::semibold(t.font_button),
|
||||
crate::ui_kit::type_face::semibold(t.font_button),
|
||||
t.form_ink,
|
||||
(text_width - version_galley.size().x - t.space_8).max(1.0),
|
||||
);
|
||||
|
||||
@@ -152,7 +152,7 @@ pub(crate) fn paint_orientation(p: &Painter, rect: Rect, y_up: bool) {
|
||||
arrow(&p, centre, end, colour, c.width(2.3), c.width(6.5));
|
||||
let text_pos = if y < 20.0 { c.at(x, y - 6.0) } else { c.at(x + if x < 60.0 { -7.0 } else { 7.0 }, y + 3.0) };
|
||||
let ink = if solidcraft_theme::contrast_ratio(colour, t.viewport_top) >= 4.5 { colour } else { t.form_ink };
|
||||
p.text(text_pos, egui::Align2::CENTER_CENTER, label, crate::fonts::semibold(t.font_small), ink);
|
||||
p.text(text_pos, egui::Align2::CENTER_CENTER, label, crate::ui_kit::type_face::semibold(t.font_small), ink);
|
||||
}
|
||||
p.circle_filled(centre, c.width(2.7), t.form_ink);
|
||||
p.circle_filled(centre, c.width(1.2), t.viewport_top);
|
||||
|
||||
@@ -68,12 +68,12 @@ pub(crate) fn settings_axis_header(ui: &mut Ui) {
|
||||
let half = rect.width() * 0.5;
|
||||
for (index, label) in ["prefs.axis", "prefs.live_input"].into_iter().enumerate() {
|
||||
let cell = Rect::from_min_size(rect.min + vec2(half * index as f32, 0.0), vec2(half, rect.height()));
|
||||
paint_text(ui, cell, &solidcraft_ui_text::tr(label).to_uppercase(), crate::fonts::semibold(t.font_small), t.form_dim, false);
|
||||
paint_text(ui, cell, &solidcraft_ui_text::tr(label).to_uppercase(), crate::ui_kit::type_face::semibold(t.font_small), t.form_dim, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (column, label) in axis_columns(rect).into_iter().zip(["prefs.axis", "prefs.live_input", "", "prefs.speed", "prefs.source"]) {
|
||||
paint_text(ui, column, &solidcraft_ui_text::tr(label).to_uppercase(), crate::fonts::semibold(t.font_small), t.form_dim, false);
|
||||
paint_text(ui, column, &solidcraft_ui_text::tr(label).to_uppercase(), crate::ui_kit::type_face::semibold(t.font_small), t.form_dim, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ pub(crate) fn settings_profile_button(ui: &mut Ui, label: &str, name: &str, edit
|
||||
let text_right = (chevron.left() - t.space_6 - if edited { t.space_8 + t.space_6 } else { 0.0 }).max(text_left);
|
||||
let overline = Rect::from_min_max(pos2(text_left, rect.center().y - t.space_16), pos2(text_right, rect.center().y - t.space_2));
|
||||
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);
|
||||
super::text::paint_text(ui, overline, &label.to_uppercase(), crate::ui_kit::type_face::semibold(t.font_panel), t.form_dim, true);
|
||||
super::text::paint_text(ui, value, name, crate::ui_kit::type_face::semibold(t.font_label), t.form_ink, false);
|
||||
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);
|
||||
@@ -114,14 +114,21 @@ pub(crate) fn settings_state_badge(ui: &mut Ui, label: &str, tone: SettingsBadge
|
||||
SettingsBadgeTone::Warning => (t.pref_note, t.pref_note_fill),
|
||||
SettingsBadgeTone::Success => (t.pref_ok, t.pref_ok_fill),
|
||||
};
|
||||
let galley = ui.painter().layout_no_wrap(label.to_owned(), crate::fonts::semibold(t.font_small), ink);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_owned(), crate::ui_kit::type_face::semibold(t.font_small), ink);
|
||||
let natural = galley.size().x + t.space_16;
|
||||
let width = if settings_page_active(ui) { natural.min(super::metrics::dimension(ui.max_rect().width())) } else { natural };
|
||||
let clipped = width < natural;
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, t.space_16 + t.space_2), Sense::hover());
|
||||
ui.painter().rect_filled(rect, t.radius_xl, fill);
|
||||
if clipped {
|
||||
super::text::paint_text(ui, rect.shrink2(vec2(t.space_8.min(width / 2.0), 0.0)), label, crate::fonts::semibold(t.font_small), ink, false);
|
||||
super::text::paint_text(
|
||||
ui,
|
||||
rect.shrink2(vec2(t.space_8.min(width / 2.0), 0.0)),
|
||||
label,
|
||||
crate::ui_kit::type_face::semibold(t.font_small),
|
||||
ink,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
ui.painter().galley(rect.center() - galley.size() / 2.0, galley, ink);
|
||||
}
|
||||
@@ -148,7 +155,7 @@ pub(crate) fn settings_chip(ui: &mut Ui, label: &str, chip: SettingsChip<'_>) ->
|
||||
let t = Tokens::get();
|
||||
let ink = if chip.accent { t.selected_ink } else { t.form_dim };
|
||||
let font = if chip.accent {
|
||||
crate::fonts::semibold(t.font_caption)
|
||||
crate::ui_kit::type_face::semibold(t.font_caption)
|
||||
} else if chip.monospace {
|
||||
FontId::monospace(t.font_caption)
|
||||
} else {
|
||||
|
||||
@@ -112,13 +112,13 @@ pub(crate) fn report_header(ui: &mut Ui, kind: &str, count: u64, time: &str, ali
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let kind_width = ui.painter().layout_no_wrap(kind.to_string(), crate::fonts::semibold(t.font_label), t.form_ink).size().x;
|
||||
let kind_width = ui.painter().layout_no_wrap(kind.to_string(), crate::ui_kit::type_face::semibold(t.font_label), t.form_ink).size().x;
|
||||
let end = (title.left() + kind_width).min((title.right() - pill_width - t.space_6).max(title.left()));
|
||||
super::text::paint_text(
|
||||
ui,
|
||||
egui::Rect::from_min_max(title.min, egui::pos2(end, title.bottom())),
|
||||
kind,
|
||||
crate::fonts::semibold(t.font_label),
|
||||
crate::ui_kit::type_face::semibold(t.font_label),
|
||||
t.form_ink,
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -295,7 +295,7 @@ impl super::MenuItem<'_> {
|
||||
SearchBadge::Valid => t.form_success,
|
||||
SearchBadge::Invalid => t.form_error,
|
||||
};
|
||||
let galley = painter.layout_no_wrap(self.shortcut.to_owned(), crate::fonts::semibold(t.font_small), ink);
|
||||
let galley = painter.layout_no_wrap(self.shortcut.to_owned(), crate::ui_kit::type_face::semibold(t.font_small), ink);
|
||||
let size = galley.size() + vec2(t.space_16, t.space_4);
|
||||
let rect = Rect::from_min_size(pos2(right - size.x, r.center().y - size.y / 2.0), size);
|
||||
painter.rect_filled(
|
||||
@@ -337,7 +337,11 @@ impl super::MenuItem<'_> {
|
||||
.flatten();
|
||||
if let Some((start, end)) = hit {
|
||||
job.append(label.get(..start).unwrap_or_default(), 0.0, normal.clone());
|
||||
job.append(label.get(start..end).unwrap_or_default(), 0.0, egui::TextFormat::simple(crate::fonts::bold(t.font_label), t.selected_ink));
|
||||
job.append(
|
||||
label.get(start..end).unwrap_or_default(),
|
||||
0.0,
|
||||
egui::TextFormat::simple(crate::ui_kit::type_face::bold(t.font_label), t.selected_ink),
|
||||
);
|
||||
job.append(label.get(end..).unwrap_or_default(), 0.0, normal);
|
||||
} else {
|
||||
job.append(label, 0.0, normal);
|
||||
|
||||
@@ -205,7 +205,7 @@ pub(crate) fn sphere_preview_with_source<R>(
|
||||
ui.spacing_mut().item_spacing.y = t.space_4;
|
||||
ui.add_sized(
|
||||
vec2(text_width, t.space_16),
|
||||
egui::Label::new(egui::RichText::new(name).font(crate::fonts::semibold(t.font_label)).color(t.form_ink))
|
||||
egui::Label::new(egui::RichText::new(name).font(crate::ui_kit::type_face::semibold(t.font_label)).color(t.form_ink))
|
||||
.truncate()
|
||||
.halign(egui::Align::Min),
|
||||
)
|
||||
@@ -254,7 +254,7 @@ impl SphereTile<'_> {
|
||||
let t = Tokens::get();
|
||||
let accent = solidcraft_theme::TabAccent::current(ui.ctx());
|
||||
let side = t.control_height + t.space_6;
|
||||
let line_height = ui.fonts_mut(|fonts| fonts.row_height(&crate::fonts::semibold(t.font_small)));
|
||||
let line_height = ui.fonts_mut(|fonts| fonts.row_height(&crate::ui_kit::type_face::semibold(t.font_small)));
|
||||
let height = t.space_16 + side + t.space_6 + line_height * 2.0 + t.space_3;
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover());
|
||||
let response = ui.interact(rect, ui.make_persistent_id(self.id), Sense::click_and_drag());
|
||||
@@ -271,20 +271,20 @@ impl SphereTile<'_> {
|
||||
cell.left_top() + vec2(t.space_4, t.space_4),
|
||||
egui::Align2::LEFT_TOP,
|
||||
badge,
|
||||
crate::fonts::semibold(t.font_badge),
|
||||
crate::ui_kit::type_face::semibold(t.font_badge),
|
||||
t.form_dim,
|
||||
);
|
||||
}
|
||||
if self.users > 0 {
|
||||
let text = if self.users > 99 { "99+".into() } else { self.users.to_string() };
|
||||
let galley = ui.painter().layout_no_wrap(text, crate::fonts::semibold(t.font_badge), t.on_accent);
|
||||
let galley = ui.painter().layout_no_wrap(text, crate::ui_kit::type_face::semibold(t.font_badge), t.on_accent);
|
||||
let width = (galley.size().x + t.space_8).max(t.space_16);
|
||||
let badge = Rect::from_min_size(pos2(cell.right() - t.space_4 - width, cell.top() + t.space_2), vec2(width, t.space_16));
|
||||
ui.painter().rect_filled(badge, t.radius_lg, accent.ink);
|
||||
ui.painter().galley(badge.center() - galley.size() / 2.0, galley, t.on_accent);
|
||||
}
|
||||
let label = Rect::from_min_max(pos2(rect.left() + t.space_3, preview.bottom() + t.space_6), rect.max - vec2(t.space_3, t.space_3));
|
||||
let font = if self.selected { crate::fonts::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) };
|
||||
let font = if self.selected { crate::ui_kit::type_face::semibold(t.font_small) } else { egui::FontId::proportional(t.font_small) };
|
||||
let mut job = egui::text::LayoutJob::simple(self.name.to_owned(), font, t.form_ink, label.width());
|
||||
job.halign = egui::Align::Center;
|
||||
job.wrap.max_rows = 2;
|
||||
|
||||
@@ -75,10 +75,10 @@ impl StageBar {
|
||||
ui,
|
||||
Rect::from_center_size(
|
||||
center + vec2(0.0, t.space_8),
|
||||
vec2(ui.painter().layout_no_wrap(label.into(), crate::fonts::semibold(t.font_button), accent.ink).size().x, t.space_16),
|
||||
vec2(ui.painter().layout_no_wrap(label.into(), crate::ui_kit::type_face::semibold(t.font_button), accent.ink).size().x, t.space_16),
|
||||
),
|
||||
label,
|
||||
crate::fonts::semibold(t.font_button),
|
||||
crate::ui_kit::type_face::semibold(t.font_button),
|
||||
accent.ink,
|
||||
false,
|
||||
);
|
||||
@@ -111,7 +111,7 @@ impl StageBar {
|
||||
pub(crate) fn caption(ui: &mut Ui, label: &str) {
|
||||
let t = Tokens::get();
|
||||
let label = label.to_uppercase();
|
||||
let font = crate::fonts::semibold(t.font_small);
|
||||
let font = crate::ui_kit::type_face::semibold(t.font_small);
|
||||
let width = ui.painter().layout_no_wrap(label.clone(), font.clone(), t.text_dim).size().x
|
||||
+ t.font_small * t.font_section_tracking * label.chars().count() as f32
|
||||
+ t.space_8;
|
||||
@@ -190,7 +190,7 @@ impl StageBar {
|
||||
t.secondary_hline(ui.painter(), line.x_range(), line.center().y, t.control_border);
|
||||
}
|
||||
let font = egui::FontId::proportional(t.font_button);
|
||||
let width = ui.painter().layout_no_wrap((*label).into(), crate::fonts::semibold(t.font_button), t.text).size().x.ceil()
|
||||
let width = ui.painter().layout_no_wrap((*label).into(), crate::ui_kit::type_face::semibold(t.font_button), t.text).size().x.ceil()
|
||||
+ t.space_8
|
||||
+ t.space_14
|
||||
+ t.space_6
|
||||
@@ -214,11 +214,11 @@ impl StageBar {
|
||||
ui,
|
||||
{
|
||||
let number = (index + 1).to_string();
|
||||
let width = ui.painter().layout_no_wrap(number, crate::fonts::semibold(t.font_small), t.text).size().x;
|
||||
let width = ui.painter().layout_no_wrap(number, crate::ui_kit::type_face::semibold(t.font_small), t.text).size().x;
|
||||
Rect::from_center_size(badge.center(), vec2(width, badge.height()))
|
||||
},
|
||||
&(index + 1).to_string(),
|
||||
crate::fonts::semibold(t.font_small),
|
||||
crate::ui_kit::type_face::semibold(t.font_small),
|
||||
if on { t.on_accent } else { t.text_dim },
|
||||
false,
|
||||
);
|
||||
@@ -227,7 +227,7 @@ impl StageBar {
|
||||
ui,
|
||||
text,
|
||||
label,
|
||||
if on { crate::fonts::semibold(t.font_button) } else { font },
|
||||
if on { crate::ui_kit::type_face::semibold(t.font_button) } else { font },
|
||||
if on { accent.ink } else { t.text },
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Status heading and constraint meter; Tokens: font_label, text, text_dim, accent, dialog_rule, sketch_accent, spacing.
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use egui::{RichText, vec2};
|
||||
use solidcraft_theme::Tokens;
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) fn steps(ui: &mut egui::Ui, key: &str, labels: &[&str], current: usiz
|
||||
ui.allocate_exact_size(vec2(super::metrics::dimension(ui.available_width()), super::row_height()), egui::Sense::hover());
|
||||
let order = i.cmp(¤t);
|
||||
let (font, ink) = match order {
|
||||
std::cmp::Ordering::Equal => (crate::fonts::semibold(t.font_label), t.form_ink),
|
||||
std::cmp::Ordering::Equal => (crate::ui_kit::type_face::semibold(t.font_label), t.form_ink),
|
||||
std::cmp::Ordering::Less => (egui::FontId::proportional(t.font_label), t.form_ink),
|
||||
std::cmp::Ordering::Greater => (egui::FontId::proportional(t.font_label), t.form_dim),
|
||||
};
|
||||
|
||||
@@ -374,7 +374,7 @@ pub(crate) fn sort_header(ui: &mut Ui, label: &str, sort: &mut Sort) -> Response
|
||||
ui.painter().rect_filled(rect, t.control_radius, t.hover);
|
||||
}
|
||||
let text = solidcraft_ui_text::tr_label(label);
|
||||
let font = crate::fonts::semibold(t.font_small);
|
||||
let font = crate::ui_kit::type_face::semibold(t.font_small);
|
||||
let width = ui.painter().layout_no_wrap(text.to_owned(), font.clone(), t.form_dim).size().x.ceil();
|
||||
let text_rect = egui::Rect::from_min_max(rect.min, egui::pos2((rect.left() + width).min(rect.right() - t.space_14 - t.space_4), rect.bottom()));
|
||||
super::text::paint_text(ui, text_rect, text, font, t.form_dim, false);
|
||||
@@ -465,7 +465,7 @@ pub(crate) fn active_pill(ui: &mut Ui) {
|
||||
let width = label_width(ui, label) + t.space_16;
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, t.font_small + t.space_6), egui::Sense::hover());
|
||||
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);
|
||||
super::text::paint_text(ui, rect.shrink2(vec2(t.space_8, 0.0)), label, crate::ui_kit::type_face::semibold(t.font_small), t.form_primary, false);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), label));
|
||||
solidcraft_ui_registry::publish_handle(&format!("dialog:inline:{label}"), rect.center());
|
||||
solidcraft_ui_registry::publish_rect(&format!("dialog:inline:{label}"), rect);
|
||||
|
||||
@@ -305,7 +305,13 @@ pub(crate) fn theme_slot(ui: &mut Ui, slot: &ThemeSlot<'_>) -> Response {
|
||||
glyph(ui.painter(), icon, if slot.night { ThemeGlyph::Moon } else { ThemeGlyph::Sun }, t.form_dim);
|
||||
let p = ui.painter().with_clip_rect(r.shrink(t.space_8).intersect(ui.clip_rect()));
|
||||
p.text(pos2(icon.right() + t.space_4, icon.center().y), Align2::LEFT_CENTER, slot.title, FontId::proportional(t.font_small), t.form_dim);
|
||||
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(x, r.bottom() - t.space_16 - t.space_4),
|
||||
Align2::LEFT_CENTER,
|
||||
slot.name,
|
||||
crate::ui_kit::type_face::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));
|
||||
super::settings_search::remember(ui, slot.title, r);
|
||||
|
||||
@@ -326,7 +326,7 @@ pub(crate) fn tree_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
|
||||
row.color.unwrap_or(if row.dim { t.text_dim } else { t.text })
|
||||
};
|
||||
let label_display = if row.name { crate::i18n::tr_name(row.label) } else { crate::i18n::tr_dyn(row.label) };
|
||||
let font = if row.selected { crate::fonts::semibold(t.font_label) } else { FontId::proportional(t.font_label) };
|
||||
let font = if row.selected { crate::ui_kit::type_face::semibold(t.font_label) } else { FontId::proportional(t.font_label) };
|
||||
// The label stops before the badges and the card's edge; a longer name ends in an ellipsis
|
||||
// and its full text is in the tooltip.
|
||||
let badges_width = row.badges.len() as f32 * 15.0;
|
||||
@@ -451,7 +451,9 @@ pub(crate) fn tree_header(ui: &mut egui::Ui, title: &str, lens: &mut usize, labe
|
||||
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)
|
||||
.map(|label| {
|
||||
ui.painter().layout_no_wrap((*label).into(), crate::ui_kit::type_face::semibold(t.font_small), t.text).size().x + t.space_16 + t.space_4
|
||||
})
|
||||
.sum::<f32>();
|
||||
let lens_rect = Rect::from_min_max(
|
||||
pos2((search.left() - t.space_8 - lens_width).max(band.left() + t.space_24 * 2.0), band.center().y - (t.control_height - t.space_4) / 2.0),
|
||||
@@ -475,7 +477,7 @@ pub(crate) fn tree_header(ui: &mut egui::Ui, title: &str, lens: &mut usize, labe
|
||||
);
|
||||
let title_rect =
|
||||
Rect::from_min_max(band.min + vec2(t.space_12, 0.0), pos2((lens_rect.left() - t.space_8).max(band.left() + t.space_12), band.bottom()));
|
||||
super::text::paint_text(ui, title_rect, title, crate::fonts::semibold(t.font_heading), t.text, false);
|
||||
super::text::paint_text(ui, title_rect, title, crate::ui_kit::type_face::semibold(t.font_heading), t.text, false);
|
||||
t.secondary_hline(ui.painter(), band.x_range(), band.bottom(), t.dialog_rule);
|
||||
response.clicked()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
//! The kit's type API: interface weights, the family and fallback names a host installs its faces
|
||||
//! under, the script checks and the on-demand CJK font state. It holds no font data and loads
|
||||
//! nothing: the host embeds and installs the faces.
|
||||
//!
|
||||
//! Host contract: before the kit draws, the context's fonts bind the weight families. A host with
|
||||
//! faces of its own binds them in its definitions (see [`with_weight_families`]); a host with none
|
||||
//! calls [`install_weight_families`] once, and the kit draws every weight with egui's default
|
||||
//! faces.
|
||||
|
||||
use egui::{FontDefinitions, FontFamily, FontId, RichText};
|
||||
|
||||
/// Name of the medium (500) interface family, and of its face in a host's definitions.
|
||||
pub const MEDIUM_FAMILY: &str = "ui-medium";
|
||||
|
||||
/// Name of the semibold (600) interface family, and of its face in a host's definitions.
|
||||
pub const SEMIBOLD_FAMILY: &str = "ui-semibold";
|
||||
|
||||
/// Name of the bold (700) interface family, and of its face in a host's definitions.
|
||||
pub const BOLD_FAMILY: &str = "ui-bold";
|
||||
|
||||
fn medium_family() -> FontFamily {
|
||||
FontFamily::Name(MEDIUM_FAMILY.into())
|
||||
}
|
||||
|
||||
fn semibold_family() -> FontFamily {
|
||||
FontFamily::Name(SEMIBOLD_FAMILY.into())
|
||||
}
|
||||
|
||||
fn bold_family() -> FontFamily {
|
||||
FontFamily::Name(BOLD_FAMILY.into())
|
||||
}
|
||||
|
||||
/// The interface families, regular first: Proportional, then medium, semibold and bold.
|
||||
pub fn sans_families() -> [FontFamily; 4] {
|
||||
[FontFamily::Proportional, medium_family(), semibold_family(), bold_family()]
|
||||
}
|
||||
|
||||
/// `fonts` with each weight family it lacks bound to its Proportional list, so the kit's weights
|
||||
/// draw with whatever regular faces the definitions have. A family already bound is kept.
|
||||
pub fn with_weight_families(mut fonts: FontDefinitions) -> FontDefinitions {
|
||||
let regular = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
|
||||
for family in [medium_family(), semibold_family(), bold_family()] {
|
||||
fonts.families.entry(family).or_insert_with(|| regular.clone());
|
||||
}
|
||||
fonts
|
||||
}
|
||||
|
||||
/// Make `ctx` ready for the kit's type with egui's default faces: the one call a host that installs
|
||||
/// no faces of its own makes before the kit draws. It replaces the context's font definitions with
|
||||
/// egui's defaults: egui offers no way to read the definitions a context will use (those set this
|
||||
/// frame stay private until the next one), so it cannot merge into them. A host that installs
|
||||
/// faces passes its own definitions through [`with_weight_families`] (or binds the weight families
|
||||
/// itself) instead.
|
||||
/// Without either, egui finds no face for a weight family (it logs the family as not bound) and
|
||||
/// draws no glyphs for the kit's weighted text.
|
||||
pub fn install_weight_families(ctx: &egui::Context) {
|
||||
ctx.set_fonts(with_weight_families(FontDefinitions::default()));
|
||||
}
|
||||
|
||||
/// Medium (500) interface text, retaining the language fallbacks and user override.
|
||||
pub fn medium(size: f32) -> FontId {
|
||||
FontId::new(size, medium_family())
|
||||
}
|
||||
|
||||
/// Semibold (600) interface text, retaining the language fallbacks and user override.
|
||||
pub fn semibold(size: f32) -> FontId {
|
||||
FontId::new(size, semibold_family())
|
||||
}
|
||||
|
||||
/// Bold (700) interface text, retaining the language fallbacks and user override.
|
||||
pub fn bold(size: f32) -> FontId {
|
||||
FontId::new(size, bold_family())
|
||||
}
|
||||
|
||||
/// Select a real interface weight without changing the text's size or colour.
|
||||
pub trait RichTextExt {
|
||||
fn medium(self) -> Self;
|
||||
fn semibold(self) -> Self;
|
||||
fn bold_face(self) -> Self;
|
||||
}
|
||||
|
||||
impl RichTextExt for RichText {
|
||||
fn medium(self) -> Self {
|
||||
self.family(medium_family())
|
||||
}
|
||||
fn semibold(self) -> Self {
|
||||
self.family(semibold_family())
|
||||
}
|
||||
fn bold_face(self) -> Self {
|
||||
self.family(bold_family())
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the CJK fallback entry in [`egui::FontDefinitions`].
|
||||
pub const CJK_FALLBACK: &str = "cjk_fallback";
|
||||
|
||||
/// Name of the Chinese fallback entry in [`egui::FontDefinitions`], after [`CJK_FALLBACK`].
|
||||
/// A Japanese-only CJK font (Meiryo, Hiragino Gothic) lacks simplified characters, so
|
||||
/// Chinese falls through to this one per glyph.
|
||||
pub const CHINESE_FALLBACK: &str = "chinese_fallback";
|
||||
|
||||
/// Name of the chosen interface font in [`egui::FontDefinitions`], first in every sans
|
||||
/// family when one is set (#1290).
|
||||
pub const UI_FONT: &str = "ui_font";
|
||||
|
||||
/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], after the CJK ones.
|
||||
pub const HANGUL_FALLBACK: &str = "hangul_fallback";
|
||||
|
||||
/// Name of the Thai fallback entry in [`egui::FontDefinitions`], after the CJK ones (#1902).
|
||||
pub const THAI_FALLBACK: &str = "thai_fallback";
|
||||
|
||||
/// Name of the Hebrew fallback entry in [`egui::FontDefinitions`], last (#3457).
|
||||
pub const HEBREW_FALLBACK: &str = "hebrew_fallback";
|
||||
|
||||
/// Name of the Arabic fallback entry in [`egui::FontDefinitions`]: first of the fallbacks for an
|
||||
/// Arabic UI, last otherwise. It stays after the Latin interface font, which keeps drawing spaces,
|
||||
/// digits and Latin text, so each Arabic word is one shaped run (see [`solidcraft_ui_text::bidi`]).
|
||||
pub const ARABIC_FALLBACK: &str = "arabic_fallback";
|
||||
|
||||
/// egui's built-in monospace face, also the last fallback of every sans family (#2955).
|
||||
pub const SYMBOL_FALLBACK: &str = "Hack";
|
||||
|
||||
/// UI languages whose text needs a CJK script font on the web.
|
||||
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "zh-TW", "ko"];
|
||||
|
||||
/// Whether the UI language needs a CJK font (the web app fetches one only then).
|
||||
pub fn needs_cjk_font(language: &str) -> bool {
|
||||
CJK_LANGUAGES.contains(&language)
|
||||
}
|
||||
|
||||
/// Whether `text` has characters only the CJK fallback draws: kana, CJK ideographs and their
|
||||
/// punctuation, and full-width forms. Hangul is left out: the web app's font has none.
|
||||
pub fn has_cjk(text: &str) -> bool {
|
||||
text.chars().any(|c| {
|
||||
matches!(u32::from(c),
|
||||
0x3000..=0x30FF // CJK punctuation, Hiragana, Katakana
|
||||
| 0x31F0..=0x31FF // Katakana phonetic extensions
|
||||
| 0x3400..=0x4DBF // CJK Extension A
|
||||
| 0x4E00..=0x9FFF // CJK Unified Ideographs
|
||||
| 0xF900..=0xFAFF // CJK Compatibility Ideographs
|
||||
| 0xFF00..=0xFFEF // Half-width and full-width forms
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether any text painted so far this frame has CJK characters (see [`has_cjk`]). The web app
|
||||
/// calls it after drawing the UI, until its CJK font is loaded, so a Japanese design name, file
|
||||
/// name or typed text fetches the font whatever the UI language (#720).
|
||||
pub fn painted_cjk(ctx: &egui::Context) -> bool {
|
||||
let mut layers: Vec<egui::LayerId> = ctx.memory(|m| m.layer_ids().collect());
|
||||
layers.push(egui::LayerId::background());
|
||||
ctx.graphics(|g| layers.iter().filter_map(|l| g.get(*l)).any(|list| list.all_entries().any(|c| shape_has_cjk(&c.shape))))
|
||||
}
|
||||
|
||||
fn shape_has_cjk(shape: &egui::Shape) -> bool {
|
||||
match shape {
|
||||
egui::Shape::Text(t) => has_cjk(&t.galley.job.text),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().any(shape_has_cjk),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Session state of the web app's on-demand script fonts. Pure state so it can be tested off the
|
||||
/// web: the shell does the fetch when [`CjkFontLoader::tick`] asks for it.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CjkFontLoader {
|
||||
status: CjkFontStatus,
|
||||
/// The saved language while this session falls back to English after a failed load.
|
||||
saved_language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum CjkFontStatus {
|
||||
#[default]
|
||||
Unloaded,
|
||||
Loading,
|
||||
Installed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl CjkFontLoader {
|
||||
pub fn status(&self) -> CjkFontStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
/// Called every frame with the live language (a `system` preference resolves to its catalog
|
||||
/// via [`solidcraft_ui_text::effective_language`]). `ready` is true when the shell already has the
|
||||
/// on-demand face that language needs. Returns true when a fetch should start now. After a
|
||||
/// failed load a CJK language falls back to English for this session; the saved preference is
|
||||
/// kept (see [`Self::persisted_language`]). Switching to another CJK language that still needs
|
||||
/// a face starts another fetch (#8894).
|
||||
pub fn tick(&mut self, language: &mut String, ready: bool) -> bool {
|
||||
if self.saved_language.is_some() && language != "en" {
|
||||
// The user picked a language themselves since the fallback: that is the preference now.
|
||||
self.saved_language = None;
|
||||
if self.status == CjkFontStatus::Failed {
|
||||
self.status = CjkFontStatus::Unloaded;
|
||||
}
|
||||
}
|
||||
if !needs_cjk_font(solidcraft_ui_text::effective_language(language)) {
|
||||
return false;
|
||||
}
|
||||
if ready {
|
||||
self.status = CjkFontStatus::Installed;
|
||||
return false;
|
||||
}
|
||||
match self.status {
|
||||
CjkFontStatus::Unloaded | CjkFontStatus::Installed => {
|
||||
self.status = CjkFontStatus::Loading;
|
||||
true
|
||||
}
|
||||
CjkFontStatus::Failed => {
|
||||
self.fall_back(language);
|
||||
false
|
||||
}
|
||||
CjkFontStatus::Loading => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// CJK text is on screen (whatever the UI language). Returns true when the Japanese font fetch
|
||||
/// should start now: once, and never again after a failed load (the UI language stays as it is).
|
||||
pub fn text_needs_font(&mut self) -> bool {
|
||||
if self.status == CjkFontStatus::Unloaded {
|
||||
self.status = CjkFontStatus::Loading;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the session still watches painted text for CJK characters.
|
||||
pub fn watching_text(&self) -> bool {
|
||||
self.status == CjkFontStatus::Unloaded
|
||||
}
|
||||
|
||||
/// Record the outcome of the fetch and install. A `system` preference resolves to its
|
||||
/// catalog first ([`Self::tick`]).
|
||||
pub fn finished(&mut self, installed: bool, language: &mut String) {
|
||||
if installed {
|
||||
self.status = CjkFontStatus::Installed;
|
||||
} else {
|
||||
self.status = CjkFontStatus::Failed;
|
||||
if needs_cjk_font(solidcraft_ui_text::effective_language(language)) {
|
||||
self.fall_back(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fall_back(&mut self, language: &mut String) {
|
||||
self.saved_language = Some(std::mem::replace(language, "en".to_owned()));
|
||||
}
|
||||
|
||||
/// The language to write to storage: the user's own choice, not this session's fallback.
|
||||
pub fn persisted_language<'a>(&'a self, live: &'a str) -> &'a str {
|
||||
self.saved_language.as_deref().unwrap_or(live)
|
||||
}
|
||||
}
|
||||
|
||||
/// The fallback entries in the order they join each family for a UI language.
|
||||
///
|
||||
/// Chinese goes first for `zh`: a Japanese font (Meiryo, Yu Gothic) lacks the simplified
|
||||
/// characters, so the rest of the word falls through to the Chinese font per glyph — and
|
||||
/// epaint centres each face's own line height in the row, so glyphs from two faces of very
|
||||
/// different height (Meiryo ≈ 2.0 em, Microsoft YaHei ≈ 1.4 em) sit at different heights
|
||||
/// inside one word. One face for the whole word keeps them on one baseline.
|
||||
/// Thai, Hebrew and Arabic come last whatever the language: no other face draws them. Arabic
|
||||
/// goes first for `ar` instead: a CJK font such as Arial Unicode also has Arabic letters, without
|
||||
/// the shaping data that joins them.
|
||||
pub fn fallback_order(language: &str) -> [&'static str; 6] {
|
||||
match language {
|
||||
"zh" | "zh-TW" => [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK, ARABIC_FALLBACK],
|
||||
"ko" => [HANGUL_FALLBACK, CJK_FALLBACK, CHINESE_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK, ARABIC_FALLBACK],
|
||||
"ar" => [ARABIC_FALLBACK, CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK],
|
||||
_ => [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK, HEBREW_FALLBACK, ARABIC_FALLBACK],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The kit sets every weight on a bare context with no host faces: binding the weight
|
||||
/// families to egui's own faces is enough, and the weights keep size and colour.
|
||||
#[test]
|
||||
fn the_kit_sets_type_with_egui_faces_when_no_host_fonts_are_installed() {
|
||||
let ctx = egui::Context::default();
|
||||
install_weight_families(&ctx);
|
||||
let ink = egui::Color32::from_rgb(10, 20, 30);
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
for (text, expected) in [
|
||||
(egui::RichText::new("Medium").color(ink).size(16.0).medium(), medium(16.0)),
|
||||
(egui::RichText::new("Semibold").color(ink).size(16.0).semibold(), semibold(16.0)),
|
||||
(egui::RichText::new("Bold").color(ink).size(16.0).bold_face(), bold(16.0)),
|
||||
] {
|
||||
ui.label(text.clone());
|
||||
let galley = egui::WidgetText::from(text).into_galley(ui, None, f32::INFINITY, egui::TextStyle::Body);
|
||||
assert_eq!(galley.job.sections[0].format.font_id, expected);
|
||||
assert_eq!(galley.job.sections[0].format.color, ink);
|
||||
}
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let defaults = egui::FontDefinitions::default();
|
||||
let installed = ctx.fonts(|f| f.definitions().families.clone());
|
||||
for family in sans_families() {
|
||||
assert_eq!(installed[&family], defaults.families[&egui::FontFamily::Proportional], "{family:?} draws with egui's faces");
|
||||
}
|
||||
let width = |font: egui::FontId| ctx.fonts_mut(|f| f.layout_no_wrap("Chamfer".into(), font, ink).size().x);
|
||||
assert_eq!(width(semibold(16.0)), width(egui::FontId::proportional(16.0)), "no host face: every weight is egui's regular face");
|
||||
assert!(ctx.fonts_mut(|f| f.has_glyphs(&bold(14.0), "Chamfer")));
|
||||
}
|
||||
|
||||
/// A kit widget that sets its name in semibold draws it on a bare context after only
|
||||
/// [`install_weight_families`]; without that call the weight family is unbound and egui has no
|
||||
/// face to draw the name with.
|
||||
#[test]
|
||||
fn a_kit_widget_draws_on_a_bare_context_once_the_weights_are_bound() {
|
||||
let ctx = egui::Context::default();
|
||||
install_weight_families(&ctx);
|
||||
let mut name = None;
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let status = crate::ui_kit::StatusHeader { icon: "", text: "Fully constrained", complete: true, fraction: 1.0 };
|
||||
crate::ui_kit::status_header(ui, "type_face", "Bracket", status);
|
||||
name = painted_text(&ctx).into_iter().find(|(text, _)| text == "Bracket").map(|(_, font)| font);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let Some(font) = name else { panic!("the name is painted") };
|
||||
assert_eq!(font.family, semibold(1.0).family, "the name is set in semibold");
|
||||
assert!(ctx.fonts_mut(|f| f.has_glyphs(&font, "Bracket")), "a face draws the semibold name");
|
||||
let bare = egui::Context::default();
|
||||
bare.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear();
|
||||
assert!(!bare.fonts(|f| f.definitions().families.contains_key(&font.family)), "a bare context has no semibold family");
|
||||
assert!(!bare.fonts_mut(|f| f.has_glyphs(&font, "Bracket")), "without the binding no face draws the name");
|
||||
}
|
||||
|
||||
fn painted_text(ctx: &egui::Context) -> Vec<(String, FontId)> {
|
||||
let layers: Vec<egui::LayerId> = ctx.memory(|m| m.layer_ids().collect());
|
||||
let mut found = Vec::new();
|
||||
ctx.graphics(|g| {
|
||||
for list in layers.iter().filter_map(|l| g.get(*l)) {
|
||||
for clipped in list.all_entries() {
|
||||
if let egui::Shape::Text(t) = &clipped.shape
|
||||
&& let Some(section) = t.galley.job.sections.first()
|
||||
{
|
||||
found.push((t.galley.job.text.clone(), section.format.font_id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
found
|
||||
}
|
||||
|
||||
/// A family the host already bound keeps its own list.
|
||||
#[test]
|
||||
fn weight_families_keep_what_the_host_bound() {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
fonts.families.insert(semibold(1.0).family, vec!["host-semibold".to_owned()]);
|
||||
let bound = with_weight_families(fonts);
|
||||
assert_eq!(bound.families[&semibold(1.0).family], ["host-semibold"]);
|
||||
assert_eq!(bound.families[&medium(1.0).family], bound.families[&egui::FontFamily::Proportional]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_font_is_fetched_only_for_a_cjk_language() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
for lang in ["en", "de"] {
|
||||
let mut language = lang.to_owned();
|
||||
assert!(!loader.tick(&mut language, false), "no CJK font fetch for {lang}");
|
||||
}
|
||||
let mut language = "ja".to_owned();
|
||||
assert!(loader.tick(&mut language, false), "Japanese starts the fetch");
|
||||
assert!(!loader.tick(&mut language, false), "the fetch starts once");
|
||||
loader.finished(true, &mut language);
|
||||
assert_eq!(loader.status(), CjkFontStatus::Installed);
|
||||
assert_eq!(language, "ja");
|
||||
assert!(!loader.tick(&mut language, true), "ready face needs no fetch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chinese_ui_fetches_again_after_japanese_is_installed() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
let mut language = "ja".to_owned();
|
||||
assert!(loader.tick(&mut language, false));
|
||||
loader.finished(true, &mut language);
|
||||
language = "zh".to_owned();
|
||||
assert!(loader.tick(&mut language, false), "Chinese needs its own face (#8894)");
|
||||
loader.finished(true, &mut language);
|
||||
assert!(!loader.tick(&mut language, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cjk_text_is_recognised() {
|
||||
for cjk in ["テスト", "ひらがな", "部品", "テスト", "Bracket(改)", "Plate 板"] {
|
||||
assert!(has_cjk(cjk), "{cjk} has CJK characters");
|
||||
}
|
||||
for other in ["", "Bracket", "Ø12 × 3 mm", "Größe", "Привет", "한국어", "émoji ✓"] {
|
||||
assert!(!has_cjk(other), "{other} has no CJK characters the fallback draws");
|
||||
}
|
||||
}
|
||||
|
||||
/// An English UI fetches the font once CJK text is painted (#720), and a failed fetch leaves
|
||||
/// the language alone.
|
||||
#[test]
|
||||
fn cjk_text_fetches_the_font_with_an_english_ui() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
let mut language = "en".to_owned();
|
||||
assert!(!loader.tick(&mut language, false));
|
||||
assert!(loader.watching_text());
|
||||
assert!(loader.text_needs_font(), "CJK text starts the fetch");
|
||||
assert!(!loader.text_needs_font(), "the fetch starts once");
|
||||
assert!(!loader.tick(&mut language, false), "no second fetch from the language");
|
||||
assert!(!loader.watching_text());
|
||||
loader.finished(false, &mut language);
|
||||
assert_eq!(loader.status(), CjkFontStatus::Failed);
|
||||
assert_eq!(language, "en");
|
||||
assert!(!loader.text_needs_font(), "no retry loop after a failure");
|
||||
|
||||
let mut loader = CjkFontLoader::default();
|
||||
assert!(loader.text_needs_font());
|
||||
loader.finished(true, &mut language);
|
||||
assert_eq!(loader.status(), CjkFontStatus::Installed);
|
||||
// Switching to Japanese afterwards needs no second fetch when the face is ready.
|
||||
let mut ja = "ja".to_owned();
|
||||
assert!(!loader.tick(&mut ja, true));
|
||||
}
|
||||
|
||||
/// The painted-text check finds CJK in the frame's labels and nothing else.
|
||||
#[test]
|
||||
fn painted_cjk_sees_labels_in_the_frame() {
|
||||
let ctx = egui::Context::default();
|
||||
let run = |text: &'static str| {
|
||||
let mut seen = false;
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
ui.label(text);
|
||||
seen = painted_cjk(ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
seen
|
||||
};
|
||||
assert!(!run("Untitled"));
|
||||
assert!(run("テスト"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_font_fetch_falls_back_for_the_session_without_changing_the_saved_language() {
|
||||
crate::i18n::install();
|
||||
let mut loader = CjkFontLoader::default();
|
||||
let mut language = "ja".to_owned();
|
||||
assert!(loader.tick(&mut language, false));
|
||||
loader.finished(false, &mut language);
|
||||
assert_eq!(language, "en", "this session shows English instead of tofu");
|
||||
assert_eq!(loader.persisted_language(&language), "ja", "the saved preference stays Japanese");
|
||||
assert!(!loader.tick(&mut language, false));
|
||||
assert_eq!(loader.persisted_language(&language), "ja");
|
||||
|
||||
// Choosing a language afterwards makes that the saved preference.
|
||||
language = "de".to_owned();
|
||||
assert!(!loader.tick(&mut language, false));
|
||||
assert_eq!(loader.persisted_language(&language), "de");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Persistent viewport status content. The caller owns items and dispatches returned actions.
|
||||
use super::{Button, Container, Surface};
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::BackingTooltipExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use egui::{Id, Rect, Response, RichText, Ui, vec2};
|
||||
use solidcraft_theme::Tokens;
|
||||
|
||||
@@ -199,7 +199,7 @@ pub(crate) fn viewport_status_item(ui: &mut Ui, item: &StatusItem, state: &mut S
|
||||
.show_frame(ui, frame, ui.max_rect(), |ui| {
|
||||
// Bound the cross-axis before laying out any part. Otherwise a later native
|
||||
// button can grow the row after its earlier, shorter contents were positioned.
|
||||
let name_height = ui.painter().layout_no_wrap(item.name.clone(), crate::fonts::semibold(t.font_label), t.text).size().y;
|
||||
let name_height = ui.painter().layout_no_wrap(item.name.clone(), crate::ui_kit::type_face::semibold(t.font_label), t.text).size().y;
|
||||
let setting_height = ui.painter().layout_no_wrap(item.setting.clone(), egui::FontId::monospace(t.font_small), t.text_dim).size().y;
|
||||
let height = t.control_height.max(ui.spacing().interact_size.y).max(name_height).max(setting_height);
|
||||
let size = vec2(ui.available_width(), height);
|
||||
@@ -357,7 +357,7 @@ pub(crate) fn viewport_status_summary(
|
||||
|ui, _| {
|
||||
let extra =
|
||||
solidcraft_ui_text::format("ui.viewport_status_summary_more", &[("count", &items.len().saturating_sub(1).to_string())]);
|
||||
let name = ui.painter().layout_no_wrap(latest.name.clone(), crate::fonts::semibold(t.font_label), t.text);
|
||||
let name = ui.painter().layout_no_wrap(latest.name.clone(), crate::ui_kit::type_face::semibold(t.font_label), t.text);
|
||||
let count = ui.painter().layout_no_wrap(extra, egui::FontId::monospace(t.font_small), t.text_dim);
|
||||
let attention = items.iter().any(|item| item.kind == StatusKind::Attention);
|
||||
let width = name.size().x + count.size().x + t.space_24 * 2.0 + t.space_8 * 4.0 + if attention { t.space_12 } else { 0.0 };
|
||||
|
||||
@@ -16,7 +16,7 @@ use serde_json::json;
|
||||
use solidcraft_engine::render::{Camera, ViewAngles};
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::fonts::RichTextExt as _;
|
||||
use crate::ui_kit::type_face::RichTextExt as _;
|
||||
use crate::ui_kit::{self, Container, Surface};
|
||||
use solidcraft_theme::Tokens;
|
||||
|
||||
|
||||
@@ -6220,7 +6220,7 @@ fn paint_toast_ext(
|
||||
// The × keeps the top-right corner: the text stops short of its ring.
|
||||
let radius = toast_close_radius();
|
||||
let close_room = if extras.close.is_some() { 2.0 * radius + t.space_4 + t.space_8 - 12.0 } else { 0.0 };
|
||||
let galley = painter.layout(text, crate::fonts::medium(t.font_label), t.text, (wrap - close_room).max(0.0));
|
||||
let galley = painter.layout(text, crate::ui_kit::type_face::medium(t.font_label), t.text, (wrap - close_room).max(0.0));
|
||||
// The buttons: each as wide as its text and a margin, in rows no wider than the text may be.
|
||||
let gap = t.space_8;
|
||||
let button_height = crate::ui_kit::row_height();
|
||||
@@ -6232,7 +6232,7 @@ fn paint_toast_ext(
|
||||
.map(|(label, kind)| {
|
||||
let width = toast_button_width(ui.ctx(), label, *kind).unwrap_or_else(|| {
|
||||
let padding = if *kind == ToastKind::Primary { t.space_16 } else { t.space_12 };
|
||||
painter.layout_no_wrap((*label).to_string(), crate::fonts::medium(t.font_label), t.text).size().x + 2.0 * padding
|
||||
painter.layout_no_wrap((*label).to_string(), crate::ui_kit::type_face::medium(t.font_label), t.text).size().x + 2.0 * padding
|
||||
});
|
||||
(width.min(row_width), *kind)
|
||||
})
|
||||
@@ -6320,7 +6320,7 @@ fn paint_toast_ext(
|
||||
let mut close = None;
|
||||
let mut closed = false;
|
||||
if let Some(handle) = extras.close {
|
||||
let line = painter.layout_no_wrap("A".into(), crate::fonts::medium(t.font_label), t.text).size().y;
|
||||
let line = painter.layout_no_wrap("A".into(), crate::ui_kit::type_face::medium(t.font_label), t.text).size().y;
|
||||
let centre = pos2(r.right() - t.space_8 - radius, (text_top.y + line * 0.5).max(r.top() + 6.0 + radius));
|
||||
let at = Rect::from_center_size(centre, vec2(2.0 * radius, 2.0 * radius));
|
||||
let mut close_ui = egui::Ui::new(
|
||||
|
||||
@@ -484,7 +484,12 @@ fn viewport_status_kinds_keep_shape_outline_and_typography_across_themes() {
|
||||
mono = text.galley.job.sections.first().is_some_and(|s| s.format.font_id.family == egui::FontFamily::Monospace);
|
||||
}
|
||||
Shape::Text(text) if text.galley.job.text == item.name => {
|
||||
name = text.galley.job.sections.first().is_some_and(|s| s.format.font_id == crate::fonts::semibold(t.font_label));
|
||||
name = text
|
||||
.galley
|
||||
.job
|
||||
.sections
|
||||
.first()
|
||||
.is_some_and(|s| s.format.font_id == crate::ui_kit::type_face::semibold(t.font_label));
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
@@ -702,7 +707,7 @@ fn compass_negative_ends_are_hollow_and_positive_ends_are_chunkier() {
|
||||
.collect();
|
||||
assert_eq!(letters.len(), 1, "each axis letter is painted once");
|
||||
let letter = letters[0];
|
||||
assert_eq!(letter.galley.job.sections[0].format.font_id, crate::fonts::bold(Tokens::get().font_compass));
|
||||
assert_eq!(letter.galley.job.sections[0].format.font_id, crate::ui_kit::type_face::bold(Tokens::get().font_compass));
|
||||
assert_eq!(letter.pos.x.fract(), 0.0);
|
||||
assert_eq!(letter.pos.y.fract(), 0.0);
|
||||
} else {
|
||||
@@ -991,7 +996,8 @@ fn the_shared_toast_draws_old_callers_exactly_as_before() {
|
||||
draw(&ctx, |ui| {
|
||||
let t = Tokens::get();
|
||||
let text = "A message that is long enough to wrap in the narrow cases, and not in the wide ones".to_string();
|
||||
let galley = ui.painter().layout(text.clone(), crate::fonts::medium(t.font_label), t.text, (free.width() - 40.0).min(440.0));
|
||||
let galley =
|
||||
ui.painter().layout(text.clone(), crate::ui_kit::type_face::medium(t.font_label), t.text, (free.width() - 40.0).min(440.0));
|
||||
let size = galley.size() + vec2(40.0, 16.0);
|
||||
let expected = Rect::from_center_size(pos2(free.center().x, free.bottom() - size.y * 0.5), size);
|
||||
let got = paint_toast(ui, free, text, error, computing);
|
||||
|
||||
+3
-2
@@ -30,13 +30,14 @@ palettes in `crates/theme/src/palettes.rs`. The compact scale runs from
|
||||
existing designs: `font_triad` (8), `font_compass` (13.5), `font_subtitle` (15),
|
||||
`font_intro` (16), `font_article_heading` (17), `font_title` (22), and
|
||||
`font_display` (26). Scaled labels derive from these tokens. Weight helpers in
|
||||
`fonts.rs` choose the family independently of size.
|
||||
`ui_kit/type_face.rs` (the kit's type API, which holds no font data) choose the
|
||||
family independently of size; `fonts.rs` embeds and installs the faces.
|
||||
|
||||
Dense forms also use tokens for control height/radius, label width, row gap,
|
||||
header/footer/card dimensions, icons, text insets, checkbox and action heights,
|
||||
and section letter spacing. Existing `space_*` tokens supply shared padding.
|
||||
The `font_literals_tests` source guard scans production Rust, including the
|
||||
viewport icon sources, and rejects numeric font sizes, private font families
|
||||
outside `fonts.rs`, and direct dense-form spacing literals. Its small per-file
|
||||
outside `ui_kit/type_face.rs`, and direct dense-form spacing literals. Its small per-file
|
||||
allowance table documents graphic spinner diameters; it permits no text sizes.
|
||||
Run it with `cargo test -p solidcraft-ui-egui --lib font_literals_tests`.
|
||||
|
||||
@@ -561,6 +561,18 @@ Board regression checks live in the existing test modules:
|
||||
the relocated setting's snapshot and page defaults.
|
||||
Runtime icon discovery visits More at narrow width, retaining Preferences page coverage.
|
||||
|
||||
## Type
|
||||
|
||||
The kit sets type through `ui_kit::type_face`: the weights (`medium`, `semibold`, `bold`,
|
||||
`RichTextExt`), the family and fallback names a host installs its faces under, the script checks
|
||||
and the on-demand CJK font state. It holds no font data. Host contract: before the kit draws, the
|
||||
context's fonts bind the weight families. A host with faces of its own binds them in its
|
||||
definitions (SolidCraft's `fonts.rs` installs IBM Plex at four weights; `with_weight_families`
|
||||
binds any it leaves out); a host with none calls `type_face::install_weight_families(ctx)` once,
|
||||
which replaces the definitions with egui's defaults, and the kit draws every weight with those
|
||||
faces. Without either, egui finds no face for the weight families and draws no glyphs for
|
||||
weighted text.
|
||||
|
||||
## Extending and checking the kit
|
||||
|
||||
Add one implementation, document its tokens/options here, re-export it through
|
||||
|
||||
Reference in New Issue
Block a user