Lesson 0007: Schematic Symbols, Text Rendering & Headless UI Testing (egui_kittest)
Drawing IC reference designators (U1, R12, J1) with Painter::text, mastering String vs. &str, and un-skipping eframe::App::ui with egui_kittest::Harness
Welcome to Act II! In Lesson 0006, we built interactive pad hit-testing and net highlighting. But, staring at anonymous red and blue rectangles gets old fast—when you are reviewing a real board or schematic, you navigate by reference designators (U1, R12, J1), component values (LDO_3V3, 10k), and human-readable net names (VCC_3V3, SPI_CLK).
Or, moreover, Lesson 0007 marks a huge milestone in our testing story. Remember how we excluded <impl eframe::App for PcbReviewerApp>::ui in .cargo/mutants.toml back in Lesson 0001? Today, we add egui_kittest to [dev-dependencies], boot our PcbReviewerApp headlessly inside cargo test, and take eframe::App::ui off the exclusion list so cargo-mutants actively guards our rendered UI panels.
flowchart LR
Comp["ComponentSymbol<br/>ref_des: String ('U1')<br/>value: String ('LDO_3V3')"] -->|"format!('{}: {} ({})')"| Panel["Panel::left<br/>ui.label(comp.inspector_label())"]
Comp -->|"Painter::text(&comp.ref_des)"| Canvas["CentralPanel Canvas<br/>Align2::CENTER_CENTER + FontId::monospace"]
Panel -->|"AccessKit Node Tree"| Kittest["egui_kittest::Harness<br/>harness.get_by_label('U1: LDO_3V3 (SOT-23)')"]
Figure 7.1: Component metadata feeds both Painter::text on the 2D canvas and semantic ui.label nodes verified headlessly by egui_kittest::Harness.
1. Just-in-Time Rust: Owned String vs. Borrowed &str & format!()
When modelling a component symbol or net label in Rust, you will immediately run into two string types (Rust Book §8.2):Because &String automatically coerces to &str (via the Deref trait), a function that accepts &str can take both string literals like "U1" and borrowed heap strings like &comp.ref_des without copying a single byte!
&str(String Slice): An immutable, fat-pointer view (ptr + len) borrowing existing UTF-8 bytes in memory (such as a compile-time string literal"Top Copper (F.Cu)"or a slice of aString). It isCopyand does not own heap memory.String(Owned UTF-8 Buffer): A growable, heap-allocated UTF-8 buffer (backed byVec<u8>) owned by a struct field (likepub ref_des: String) or freshly constructed at runtime with theformat!()macro.
2. Headless UI Setup with egui_kittest (Cargo.toml)
So, how do you test a GUI without opening a real OS window and hijacking your mouse cursor?
Turns out, egui_kittest solves this cleanly by tapping into AccessKit's semantic accessibility node tree. Add egui_kittest as a development dependency with its eframe feature enabled:
cargo add --dev egui_kittest --features eframe
Here is the resulting Cargo.toml:
[package]
name = "pcb-reviewer"
version = "0.1.0"
edition = "2024"
[dependencies]
eframe = "0.37"
[dev-dependencies]
egui_kittest = { version = "0.37", features = ["eframe"] }
# Optimise dependencies (like egui/eframe) even in debug builds so our
# 2D PCB canvas renders at 60 FPS while keeping our own code fast to compile.
[profile.dev.package."*"]
opt-level = 2
3. Anatomy of Lesson 0007's Additions: Block by Block
Let's unpack every piece of code added or updated in apps/0007-schematic-symbols-and-text-rendering/src/main.rs compared to Lesson 0006, one block at a time, and then review the complete listing in context.
Block 1: NetId Formatting, ComponentSymbol & Sample Footprints
use eframe::egui::{
self, Align2, Color32, FontId, Pos2, Rect, Sense, Stroke, StrokeKind, Vec2, emath::TSTransform,
pos2, vec2,
};
impl NetId {
/// Returns a freshly formatted, heap-allocated `String` net code (`"NET_1"`).
pub fn code(self) -> String {
format!("NET_{}", self.0)
}
/// Returns a zero-allocation static string slice (`&'static str`) for known signal names.
pub fn signal_name(self) -> &'static str {
match self.0 {
1 => "VCC_3V3",
2 => "SPI_CLK",
_ => "UNASSIGNED",
}
}
}
/// Silkscreen/schematic component metadata rendered on the canvas with `Painter::text`
/// and listed in the `Panel` Component Inspector.
#[derive(Clone, Debug, PartialEq)]
pub struct ComponentSymbol {
pub ref_des: String,
pub value: String,
pub package_name: String,
pub center_mm: Pos2,
pub body_size_mm: Vec2,
}
impl ComponentSymbol {
pub fn inspector_label(&self) -> String {
format!("{}: {} ({})", self.ref_des, self.value, self.package_name)
}
}
fn sample_components() -> Vec<ComponentSymbol> {
vec![
ComponentSymbol {
ref_des: "U1".to_owned(),
value: "LDO_3V3".to_owned(),
package_name: "SOT-23".to_owned(),
center_mm: pos2(20.0, 27.0),
body_size_mm: vec2(12.0, 16.0),
},
ComponentSymbol {
ref_des: "R12".to_owned(),
value: "10k".to_owned(),
package_name: "0603".to_owned(),
center_mm: pos2(52.0, 35.0),
body_size_mm: vec2(10.0, 6.0),
},
ComponentSymbol {
ref_des: "J1".to_owned(),
value: "CONN_2P".to_owned(),
package_name: "Header".to_owned(),
center_mm: pos2(68.0, 35.0),
body_size_mm: vec2(10.0, 14.0),
},
]
}
use eframe::egui::{..., Align2, FontId}: ImportsAlign2(2D anchor alignment for text bounding boxes) andFontId(font family + size in logical points) alongside our existingeguiprimitives.NetId::code(self) -> Stringvs.NetId::signal_name(self) -> &'static str: Contrasts our two Rust string types directly.codeusesformat!("NET_{}", self.0)to allocate a brand-newStringon the heap at runtime, whereassignal_namereturns a&'static strslice pointing straight into the binary's read-only data segment with zero heap allocation.#[derive(Clone, Debug, PartialEq)] pub struct ComponentSymbol: Notice we deriveClone—and notCopy—becauseref_des,value, andpackage_nameare heap-allocatedStringbuffers. Rust forbids implicit bitwiseCopyon types that own heap memory.pub fn inspector_label(&self) -> String: Borrows&selfimmutably and usesformat!("{}: {} ({})", ...)to build a formatted summary string ("U1: LDO_3V3 (SOT-23)") for the side-panel inspector."U1".to_owned()insample_components(): String literals like"U1"have type&'static str; calling.to_owned()(from theToOwnedtrait) allocates an ownedStringcopy to populate each struct field.
Block 2: Storing components & Extracting render_side_panel / render_canvas
pub struct PcbReviewerApp {
pub show_top_copper: bool,
pub show_bottom_copper: bool,
pub show_silkscreen: bool,
pub show_ratsnest: bool,
pub viewport: BoardViewport,
pub traces: Vec<TraceSegment>,
pub pads: Vec<SmdPad>,
pub components: Vec<ComponentSymbol>,
pub hovered_net: Option<NetId>,
pub selected_net: Option<NetId>,
}
impl Default for PcbReviewerApp {
fn default() -> Self {
let mut app = Self {
show_top_copper: false,
show_bottom_copper: false,
show_silkscreen: false,
show_ratsnest: false,
viewport: BoardViewport::default(),
traces: sample_traces(),
pads: sample_pads(),
components: sample_components(),
hovered_net: None,
selected_net: None,
};
app.reset_layers();
app
}
}
impl PcbReviewerApp {
// ...
pub fn render_side_panel(&mut self, ui: &mut egui::Ui) {
ui.heading("PCB Layers");
ui.checkbox(&mut self.show_top_copper, "Top Copper (F.Cu)");
ui.checkbox(&mut self.show_bottom_copper, "Bottom Copper (B.Cu)");
ui.checkbox(&mut self.show_silkscreen, "Silkscreen (F.SilkS)");
ui.checkbox(&mut self.show_ratsnest, "Ratsnest (Unrouted)");
ui.separator();
if ui.button("Isolate Top Copper").clicked() {
self.isolate_top_copper();
}
if ui.button("Show All Layers").clicked() {
self.show_all_layers();
}
ui.separator();
ui.heading("Components (RefDes)");
if self.show_silkscreen {
for comp in &self.components {
ui.label(comp.inspector_label());
}
} else {
ui.label("Enable Silkscreen to inspect component RefDes.");
}
ui.separator();
ui.heading("Net Inspector");
if let Some(net) = self.active_net() {
ui.label(format!("Active Net: {} ({})", net.code(), net.signal_name()));
if ui.button("Clear Selection").clicked() {
self.selected_net = None;
}
} else {
ui.label("Hover or click a pad to cross-probe its net.");
}
}
fn render_canvas(&mut self, ui: &mut egui::Ui) {
// Allocates painter, pans/zooms viewport, hit-tests pads, and calls self.paint_board(...)
// ...
}
}
impl eframe::App for PcbReviewerApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::left("layer_panel").show(ui, |ui| {
self.render_side_panel(ui);
});
egui::CentralPanel::default().show(ui, |ui| {
self.render_canvas(ui);
});
}
}
pub components: Vec<ComponentSymbol>: Adds the owned vector of silkscreen/schematic component symbols toPcbReviewerAppand initialises it withsample_components()insideimpl Default.- Extracting
render_side_panelandrender_canvas: Moving the left panel body intopub fn render_side_panel(&mut self, ui: &mut egui::Ui)and the viewport interaction intofn render_canvas(&mut self, ui: &mut egui::Ui)keepseframe::App::uiclean and lets.cargo/mutants.tomltargetuiandrender_side_panelfor mutation testing while excluding the raw pixel-painting methodrender_canvas. for comp in &self.components: Borrowsself.componentsimmutably (&Vec<ComponentSymbol>) so each iteration yieldscomp: &ComponentSymbolwithout moving or cloning the underlyingStringfields.ui.label(comp.inspector_label())&net.code()/net.signal_name(): Renders each component's formattedRefDes: Value (Package)summary whenself.show_silkscreenis enabled, and displays both the ownednet.code()and borrowednet.signal_name()in the Net Inspector.
Block 3: Drawing Silkscreen Outlines & Reference Designators (Painter::text)
On the 2D canvas, Painter::text renders crisp vector-rasterised glyphs anchored to any screen-space Pos2 using Align2 and FontId:Using Align2::CENTER_CENTER places the exact geometric centre of the rendered glyph bounding box at center_px, keeping U1 centred inside its IC body at any zoom level.
if self.show_silkscreen {
let silk_color = PcbLayer::Silkscreen.color();
for comp in &self.components {
let body_mm = Rect::from_center_size(comp.center_mm, comp.body_size_mm);
let screen_body = Rect::from_min_max(
self.viewport.world_to_screen(body_mm.min),
self.viewport.world_to_screen(body_mm.max),
);
painter.rect(
screen_body,
0.4 * scale,
Color32::from_rgba_unmultiplied(240, 230, 140, 14),
Stroke::new((0.25 * scale).max(1.0), silk_color),
StrokeKind::Inside,
);
let center_px = self.viewport.world_to_screen(comp.center_mm);
painter.text(
center_px,
Align2::CENTER_CENTER,
&comp.ref_des,
FontId::monospace((2.2 * scale).clamp(9.0, 22.0)),
silk_color,
);
}
}
Rect::from_center_size(comp.center_mm, comp.body_size_mm): Computes the component body rectangle in board millimetres, then projects.minand.maxinto screen pixels viaself.viewport.world_to_screen(...).Color32::from_rgba_unmultiplied(240, 230, 140, 14): Fills the component courtyard with a subtle translucent silkscreen tint (alpha = 14) bordered by aStrokeKind::Insideoutline.painter.text(center_px, Align2::CENTER_CENTER, &comp.ref_des, ...): Draws the reference designator centred overcenter_px. Notice we pass&comp.ref_des(&String), which coerces to&str(impl Into<WidgetText>) without cloning the string.FontId::monospace((2.2 * scale).clamp(9.0, 22.0)): Scales the monospace font size proportionally with the viewport zoomscalewhile clamping between9.0and22.0logical points so labels stay legible when zoomed out and never overwhelm the screen when zoomed in.
Block 4: Unit & Headless UI Verification (egui_kittest::Harness)
With egui_kittest under [dev-dependencies], we can spin up our entire eframe::App headlessly in milliseconds, query widgets by their label, and click buttons directly from cargo test:
use egui_kittest::{Harness, kittest::Queryable};
#[test]
fn net_id_code_and_signal_name_distinguish_owned_string_from_str_slice() {
assert_eq!(NetId(1).code(), "NET_1");
assert_eq!(NetId(1).signal_name(), "VCC_3V3");
assert_eq!(NetId(2).code(), "NET_2");
assert_eq!(NetId(2).signal_name(), "SPI_CLK");
assert_eq!(NetId(99).code(), "NET_99");
assert_eq!(NetId(99).signal_name(), "UNASSIGNED");
}
#[test]
fn component_symbol_inspector_label_formats_ref_des_value_and_package() {
let comp = ComponentSymbol {
ref_des: "U1".to_owned(),
value: "LDO_3V3".to_owned(),
package_name: "SOT-23".to_owned(),
center_mm: pos2(20.0, 27.0),
body_size_mm: vec2(12.0, 16.0),
};
assert_eq!(comp.inspector_label(), "U1: LDO_3V3 (SOT-23)");
}
#[test]
fn headless_ui_renders_component_labels_and_toggles_layers() {
let mut harness = Harness::new_eframe(|_cc| PcbReviewerApp::default());
assert!(harness.query_by_label("U1: LDO_3V3 (SOT-23)").is_some());
assert!(harness.query_by_label("R12: 10k (0603)").is_some());
assert!(harness.query_by_label("J1: CONN_2P (Header)").is_some());
harness.get_by_label("Isolate Top Copper").click();
harness.run();
assert!(!harness.state().show_bottom_copper);
assert!(!harness.state().show_silkscreen);
assert!(harness.query_by_label("U1: LDO_3V3 (SOT-23)").is_none());
assert!(harness
.query_by_label("Enable Silkscreen to inspect component RefDes.")
.is_some());
harness.get_by_label("Show All Layers").click();
harness.run();
assert!(harness.state().show_ratsnest);
assert!(harness.query_by_label("U1: LDO_3V3 (SOT-23)").is_some());
harness.state_mut().selected_net = Some(NetId(1));
harness.run();
assert!(harness
.query_by_label("Active Net: NET_1 (VCC_3V3)")
.is_some());
harness.get_by_label("Clear Selection").click();
harness.run();
assert_eq!(harness.state().selected_net, None);
}
use egui_kittest::{Harness, kittest::Queryable}: Brings inHarness(the headlessegui/eframerunner) and theQueryabletrait (which adds.query_by_label(...)and.get_by_label(...)to the harness).Harness::new_eframe(|_cc| PcbReviewerApp::default()): BootsPcbReviewerAppinside an offscreeneframecontext and runs the initial layout frame so the AccessKit node tree is immediately ready to query.harness.query_by_label(...)vs.harness.get_by_label(...):query_by_labelreturns anOption(is_some()/is_none()), making it ideal for asserting whether a conditional label is present or hidden.get_by_labelunwraps the node (panicking with a helpful widget tree dump if missing) so you can chain.click()on buttons like"Isolate Top Copper","Show All Layers", and"Clear Selection".harness.run()&harness.state()/harness.state_mut(): After queuing a.click()or mutating state viaharness.state_mut(), callingharness.run()steps the immediate-mode loop forward soeframe::App::uiprocesses the interaction and updates the rendered widget tree.
Complete Lesson 0007 Listing (In Context)
Here is how all four blocks assemble in apps/0007-schematic-symbols-and-text-rendering/src/main.rs alongside our existing viewport and layer logic:
use eframe::egui::{
self, Align2, Color32, FontId, Pos2, Rect, Sense, Stroke, StrokeKind, Vec2, emath::TSTransform,
pos2, vec2,
};
// ... [MIN_ZOOM, MAX_ZOOM, and fn main() unchanged from Lesson 0006] ...
/// Strongly-typed identifier for an electrical net (e.g. `NetId(1)` = `VCC_3V3`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NetId(pub u32);
impl NetId {
/// Returns a freshly formatted, heap-allocated `String` net code (`"NET_1"`).
pub fn code(self) -> String {
format!("NET_{}", self.0)
}
/// Returns a zero-allocation static string slice (`&'static str`) for known signal names.
pub fn signal_name(self) -> &'static str {
match self.0 {
1 => "VCC_3V3",
2 => "SPI_CLK",
_ => "UNASSIGNED",
}
}
}
// ... [PcbLayer, TraceSegment, SmdPad, BoardViewport, sample_traces(), sample_pads()] ...
/// Silkscreen/schematic component metadata rendered on the canvas with `Painter::text`
/// and listed in the `Panel` Component Inspector.
#[derive(Clone, Debug, PartialEq)]
pub struct ComponentSymbol {
pub ref_des: String,
pub value: String,
pub package_name: String,
pub center_mm: Pos2,
pub body_size_mm: Vec2,
}
impl ComponentSymbol {
pub fn inspector_label(&self) -> String {
format!("{}: {} ({})", self.ref_des, self.value, self.package_name)
}
}
fn sample_components() -> Vec<ComponentSymbol> {
vec![
ComponentSymbol {
ref_des: "U1".to_owned(),
value: "LDO_3V3".to_owned(),
package_name: "SOT-23".to_owned(),
center_mm: pos2(20.0, 27.0),
body_size_mm: vec2(12.0, 16.0),
},
ComponentSymbol {
ref_des: "R12".to_owned(),
value: "10k".to_owned(),
package_name: "0603".to_owned(),
center_mm: pos2(52.0, 35.0),
body_size_mm: vec2(10.0, 6.0),
},
ComponentSymbol {
ref_des: "J1".to_owned(),
value: "CONN_2P".to_owned(),
package_name: "Header".to_owned(),
center_mm: pos2(68.0, 35.0),
body_size_mm: vec2(10.0, 14.0),
},
]
}
pub struct PcbReviewerApp {
pub show_top_copper: bool,
pub show_bottom_copper: bool,
pub show_silkscreen: bool,
pub show_ratsnest: bool,
pub viewport: BoardViewport,
pub traces: Vec<TraceSegment>,
pub pads: Vec<SmdPad>,
pub components: Vec<ComponentSymbol>,
pub hovered_net: Option<NetId>,
pub selected_net: Option<NetId>,
}
impl Default for PcbReviewerApp {
fn default() -> Self {
let mut app = Self {
show_top_copper: false,
show_bottom_copper: false,
show_silkscreen: false,
show_ratsnest: false,
viewport: BoardViewport::default(),
traces: sample_traces(),
pads: sample_pads(),
components: sample_components(),
hovered_net: None,
selected_net: None,
};
app.reset_layers();
app
}
}
impl PcbReviewerApp {
// ... [reset_layers, isolate_top_copper, show_all_layers, reset_viewport,
// is_layer_visible, visible_traces_in_z_order, hit_test_net_at_mm, active_net] ...
pub fn render_side_panel(&mut self, ui: &mut egui::Ui) {
ui.heading("PCB Layers");
ui.checkbox(&mut self.show_top_copper, "Top Copper (F.Cu)");
ui.checkbox(&mut self.show_bottom_copper, "Bottom Copper (B.Cu)");
ui.checkbox(&mut self.show_silkscreen, "Silkscreen (F.SilkS)");
ui.checkbox(&mut self.show_ratsnest, "Ratsnest (Unrouted)");
ui.separator();
if ui.button("Isolate Top Copper").clicked() {
self.isolate_top_copper();
}
if ui.button("Show All Layers").clicked() {
self.show_all_layers();
}
ui.separator();
ui.heading("Components (RefDes)");
if self.show_silkscreen {
for comp in &self.components {
ui.label(comp.inspector_label());
}
} else {
ui.label("Enable Silkscreen to inspect component RefDes.");
}
ui.separator();
ui.heading("Net Inspector");
if let Some(net) = self.active_net() {
ui.label(format!("Active Net: {} ({})", net.code(), net.signal_name()));
if ui.button("Clear Selection").clicked() {
self.selected_net = None;
}
} else {
ui.label("Hover or click a pad to cross-probe its net.");
}
}
fn render_canvas(&mut self, ui: &mut egui::Ui) {
let (response, painter) =
ui.allocate_painter(ui.available_size(), Sense::click_and_drag());
if response.dragged() {
self.viewport.pan_by(response.drag_delta());
}
if let Some(hover_pos) = response.hover_pos() {
let scroll_y = ui.input(|i| i.smooth_scroll_delta.y);
if scroll_y != 0.0 {
let zoom_factor = (scroll_y * 0.005).exp();
self.viewport.zoom_around(hover_pos, zoom_factor);
}
let world_mm = self.viewport.screen_to_world(hover_pos);
self.hovered_net = self.hit_test_net_at_mm(world_mm);
if response.clicked() {
self.selected_net = self.hovered_net;
}
} else {
self.hovered_net = None;
}
self.paint_board(&painter, response.rect);
}
fn paint_board(&self, painter: &egui::Painter, canvas_rect: Rect) {
// ... [board outline, visible traces, and SMD pads painting unchanged] ...
if self.show_silkscreen {
let silk_color = PcbLayer::Silkscreen.color();
for comp in &self.components {
let body_mm = Rect::from_center_size(comp.center_mm, comp.body_size_mm);
let screen_body = Rect::from_min_max(
self.viewport.world_to_screen(body_mm.min),
self.viewport.world_to_screen(body_mm.max),
);
painter.rect(
screen_body,
0.4 * scale,
Color32::from_rgba_unmultiplied(240, 230, 140, 14),
Stroke::new((0.25 * scale).max(1.0), silk_color),
StrokeKind::Inside,
);
let center_px = self.viewport.world_to_screen(comp.center_mm);
painter.text(
center_px,
Align2::CENTER_CENTER,
&comp.ref_des,
FontId::monospace((2.2 * scale).clamp(9.0, 22.0)),
silk_color,
);
}
}
}
}
impl eframe::App for PcbReviewerApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::left("layer_panel").show(ui, |ui| {
self.render_side_panel(ui);
});
egui::CentralPanel::default().show(ui, |ui| {
self.render_canvas(ui);
});
}
}
Painter::text Anchor Alignment & egui_kittest Node Tree
Align2 • FontId • Harness::new_eframe
Adjust the Align2 anchor mode, font size, and show_silkscreen toggle below. Observe how Painter::text repositions the reference designators on the canvas while egui_kittest::Harness inspects the live accessibility tree in the Panel::left.
1. Text & Layer Controls
2. Live Code & egui_kittest Tree
inspector_label & the egui_kittest Assertion
format!() • Align2 • Queryable
Fill in the three blanks below to (1) format an owned String in inspector_label, (2) centre-align the reference designator glyph on the canvas, and (3) click the "Isolate Top Copper" button inside our headless egui_kittest test:
impl ComponentSymbol {
pub fn inspector_label(&self) -> String {
("{}: {} ({})", self.ref_des, self.value, self.package_name)
}
}
// Inside paint_board():
painter.text(
center_px,
Align2::,
&comp.ref_des,
FontId::monospace(12.0),
PcbLayer::Silkscreen.color(),
);
// Inside #[test] fn headless_ui_renders_component_labels_and_toggles_layers():
let mut harness = Harness::new_eframe(|_cc| PcbReviewerApp::default());
harness.("Isolate Top Copper").click();
harness.run();
eframe::App::ui with egui_kittest
cargo test + cargo mutants
Look at apps/0007-schematic-symbols-and-text-rendering/.cargo/mutants.toml: we have removed "<impl eframe::App for PcbReviewerApp>::ui" from exclude_re! Because our egui_kittest::Harness test queries the rendered Panel::left labels and clicks its buttons, cargo mutants now mutates ui() and render_side_panel()—and catches every single mutant.
# apps/0007-schematic-symbols-and-text-rendering/.cargo/mutants.toml
copy_target = true
# Notice that `<impl eframe::App for PcbReviewerApp>::ui` and `render_side_panel`
# are NO LONGER excluded! Only the OS window launcher and raw Painter canvas remain excluded.
exclude_re = [
"replace main",
"PcbReviewerApp::render_canvas",
"PcbReviewerApp::paint_board",
"sample_traces",
"sample_pads",
"sample_components",
]
String vs. &str, Align2, and egui_kittest
3 Questions • Equal-Length Options
Recommended Primary Source
egui_kittest::Harness Official API Docs and The Rust Programming Language — §8.2 "Storing UTF-8 Encoded Text with Strings".
egui_kittest, head to Lesson 0008: Split Viewports, egui::Scene & Borrow-Checker State Splits to build a side-by-side Schematic + PCB cross-probing workspace!