570 lines
20 KiB
Rust
570 lines
20 KiB
Rust
|
|
#![allow(deprecated)]
|
||
|
|
use eframe::egui;
|
||
|
|
use egui::{Color32, ColorImage, Pos2};
|
||
|
|
use hcie_text::TextRenderer;
|
||
|
|
use std::fs;
|
||
|
|
|
||
|
|
#[derive(Clone)]
|
||
|
|
struct TextObject {
|
||
|
|
text: String,
|
||
|
|
font: String,
|
||
|
|
size: f32,
|
||
|
|
color: [u8; 4],
|
||
|
|
pos: Pos2,
|
||
|
|
w: u32,
|
||
|
|
h: u32,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TextObject {
|
||
|
|
fn bounds(&self) -> egui::Rect {
|
||
|
|
egui::Rect::from_min_size(self.pos, egui::vec2(self.w as f32, self.h as f32))
|
||
|
|
}
|
||
|
|
fn contains(&self, point: Pos2) -> bool {
|
||
|
|
self.bounds().contains(point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Strip common style suffixes to find the font family base name.
|
||
|
|
fn extract_base_font(name: &str) -> String {
|
||
|
|
let suffixes = [
|
||
|
|
"-BoldItalic", "-BoldOblique", "-Bold", "-Italic", "-Oblique", "-Regular", "-Medium", "-Light",
|
||
|
|
"-Thin", "-Black", "-SemiBold", "-ExtraBold", "-ExtraLight",
|
||
|
|
"BoldItalic", "BoldOblique", "Bold", "Italic", "Oblique", "Regular", "Medium", "Light",
|
||
|
|
];
|
||
|
|
for suffix in &suffixes {
|
||
|
|
if name.ends_with(suffix) {
|
||
|
|
return name[..name.len() - suffix.len()].to_string();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
name.to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Detect bold/italic flags from an already-resolved font name.
|
||
|
|
fn detect_style(name: &str) -> (bool, bool) {
|
||
|
|
let bold = name.contains("Bold");
|
||
|
|
let italic = name.contains("Italic") || name.contains("Oblique");
|
||
|
|
(bold, italic)
|
||
|
|
}
|
||
|
|
|
||
|
|
struct DraftState {
|
||
|
|
text: String,
|
||
|
|
font: String, // resolved full font name (e.g. "DejaVuSans-Bold")
|
||
|
|
base_font: String, // family name (e.g. "DejaVuSans")
|
||
|
|
size: f32,
|
||
|
|
color: [u8; 4],
|
||
|
|
bold: bool,
|
||
|
|
italic: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl DraftState {
|
||
|
|
fn from_object(obj: &TextObject) -> Self {
|
||
|
|
let (bold, italic) = detect_style(&obj.font);
|
||
|
|
Self {
|
||
|
|
text: obj.text.clone(),
|
||
|
|
font: obj.font.clone(),
|
||
|
|
base_font: extract_base_font(&obj.font),
|
||
|
|
size: obj.size,
|
||
|
|
color: obj.color,
|
||
|
|
bold,
|
||
|
|
italic,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn resolve_font(&mut self, renderer: &TextRenderer) {
|
||
|
|
let candidates = match (self.bold, self.italic) {
|
||
|
|
(true, true) => vec![
|
||
|
|
format!("{}-BoldItalic", self.base_font),
|
||
|
|
format!("{}BoldItalic", self.base_font),
|
||
|
|
format!("{}-BoldOblique", self.base_font),
|
||
|
|
format!("{}BoldOblique", self.base_font),
|
||
|
|
],
|
||
|
|
(true, false) => vec![
|
||
|
|
format!("{}-Bold", self.base_font),
|
||
|
|
format!("{}Bold", self.base_font),
|
||
|
|
],
|
||
|
|
(false, true) => vec![
|
||
|
|
format!("{}-Italic", self.base_font),
|
||
|
|
format!("{}Italic", self.base_font),
|
||
|
|
format!("{}-Oblique", self.base_font),
|
||
|
|
format!("{}Oblique", self.base_font),
|
||
|
|
],
|
||
|
|
(false, false) => vec![self.base_font.clone()],
|
||
|
|
};
|
||
|
|
for c in &candidates {
|
||
|
|
if renderer.has_font(c) {
|
||
|
|
self.font = c.clone();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if let Some(first) = candidates.first() {
|
||
|
|
self.font = first.clone();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn set_base_font(&mut self, base: &str, renderer: &TextRenderer) {
|
||
|
|
self.base_font = base.to_string();
|
||
|
|
self.bold = false;
|
||
|
|
self.italic = false;
|
||
|
|
self.resolve_font(renderer);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn scan_font_dir(renderer: &mut TextRenderer, dir: &std::path::Path) {
|
||
|
|
if let Ok(entries) = fs::read_dir(dir) {
|
||
|
|
for entry in entries.flatten() {
|
||
|
|
let path = entry.path();
|
||
|
|
if path.is_dir() {
|
||
|
|
scan_font_dir(renderer, &path);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||
|
|
if ext != "ttf" && ext != "otf" {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
|
||
|
|
if let Ok(data) = fs::read(&path) {
|
||
|
|
let _ = renderer.load_font(name, &data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn load_all_fonts(renderer: &mut TextRenderer) {
|
||
|
|
let font_dirs = [
|
||
|
|
"/usr/share/fonts/truetype/dejavu",
|
||
|
|
"/usr/share/fonts/truetype/liberation",
|
||
|
|
];
|
||
|
|
for dir in &font_dirs {
|
||
|
|
scan_font_dir(renderer, std::path::Path::new(dir));
|
||
|
|
}
|
||
|
|
let fallbacks = [
|
||
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
|
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||
|
|
];
|
||
|
|
for path in &fallbacks {
|
||
|
|
if let Ok(data) = fs::read(path) {
|
||
|
|
let stem = std::path::Path::new(path)
|
||
|
|
.file_stem()
|
||
|
|
.and_then(|s| s.to_str())
|
||
|
|
.unwrap_or("fallback");
|
||
|
|
if !renderer.has_font(stem) {
|
||
|
|
let _ = renderer.load_font(stem, &data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct App {
|
||
|
|
renderer: TextRenderer,
|
||
|
|
draft: DraftState,
|
||
|
|
objects: Vec<TextObject>,
|
||
|
|
selected_index: Option<usize>,
|
||
|
|
canvas_pixels: Vec<u8>,
|
||
|
|
texture: Option<egui::TextureHandle>,
|
||
|
|
canvas_size: [u32; 2],
|
||
|
|
font_loaded: bool,
|
||
|
|
dirty: bool,
|
||
|
|
anti_alias: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl App {
|
||
|
|
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||
|
|
let mut renderer = TextRenderer::new();
|
||
|
|
load_all_fonts(&mut renderer);
|
||
|
|
let font_loaded = !renderer.get_fonts().is_empty();
|
||
|
|
let first_font = renderer.get_fonts().first().cloned().unwrap_or_default();
|
||
|
|
let base = extract_base_font(&first_font);
|
||
|
|
|
||
|
|
Self {
|
||
|
|
renderer,
|
||
|
|
draft: DraftState {
|
||
|
|
text: "Hello!".to_string(),
|
||
|
|
font: first_font.clone(),
|
||
|
|
base_font: base,
|
||
|
|
size: 64.0,
|
||
|
|
color: [0, 0, 0, 255],
|
||
|
|
bold: false,
|
||
|
|
italic: false,
|
||
|
|
},
|
||
|
|
objects: Vec::new(),
|
||
|
|
selected_index: None,
|
||
|
|
canvas_pixels: vec![255; 1024 * 1024 * 4],
|
||
|
|
texture: None,
|
||
|
|
canvas_size: [1024, 1024],
|
||
|
|
font_loaded,
|
||
|
|
dirty: true,
|
||
|
|
anti_alias: true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn redraw(&mut self) {
|
||
|
|
if !self.font_loaded {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let w = self.canvas_size[0] as usize;
|
||
|
|
let h = self.canvas_size[1] as usize;
|
||
|
|
self.canvas_pixels.fill(255);
|
||
|
|
|
||
|
|
for (_i, obj) in self.objects.iter().enumerate() {
|
||
|
|
if let Ok((pixels, tw, th, _, _, _, _)) =
|
||
|
|
self.renderer
|
||
|
|
.rasterize_text(&obj.text, &obj.font, obj.size, obj.color, 0.0, 0.0, 0.0, hcie_protocol::tools::TextAlignment::Left, hcie_protocol::tools::TextOrientation::Horizontal, &[], self.anti_alias)
|
||
|
|
{
|
||
|
|
let tx = obj.pos.x as i32;
|
||
|
|
let ty = obj.pos.y as i32;
|
||
|
|
for py in 0..th as i32 {
|
||
|
|
for px in 0..tw as i32 {
|
||
|
|
let cx = tx + px;
|
||
|
|
let cy = ty + py;
|
||
|
|
if cx >= 0 && cx < w as i32 && cy >= 0 && cy < h as i32 {
|
||
|
|
let si = ((py as u32 * tw + px as u32) * 4) as usize;
|
||
|
|
let di = (cy as usize * w + cx as usize) * 4;
|
||
|
|
let a = pixels[si + 3] as f32 / 255.0;
|
||
|
|
if a <= 0.0 {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
for i in 0..4 {
|
||
|
|
let s = pixels[si + i] as f32;
|
||
|
|
let d = self.canvas_pixels[di + i] as f32;
|
||
|
|
self.canvas_pixels[di + i] = (s * a + d * (1.0 - a)) as u8;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.dirty = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
fn apply_draft(&mut self) {
|
||
|
|
let Some(idx) = self.selected_index else { return };
|
||
|
|
if idx >= self.objects.len() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if let Ok((_pixels, tw, th, _, _, _, _)) = self.renderer.rasterize_text(
|
||
|
|
&self.draft.text,
|
||
|
|
&self.draft.font,
|
||
|
|
self.draft.size,
|
||
|
|
self.draft.color,
|
||
|
|
0.0,
|
||
|
|
0.0,
|
||
|
|
0.0,
|
||
|
|
hcie_protocol::tools::TextAlignment::Left,
|
||
|
|
hcie_protocol::tools::TextOrientation::Horizontal,
|
||
|
|
&[],
|
||
|
|
self.anti_alias,
|
||
|
|
) {
|
||
|
|
self.objects[idx] = TextObject {
|
||
|
|
text: self.draft.text.clone(),
|
||
|
|
font: self.draft.font.clone(),
|
||
|
|
size: self.draft.size,
|
||
|
|
color: self.draft.color,
|
||
|
|
pos: self.objects[idx].pos,
|
||
|
|
w: tw,
|
||
|
|
h: th,
|
||
|
|
};
|
||
|
|
self.redraw();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn select_object(&mut self, idx: usize) {
|
||
|
|
// Auto-apply previous selection before switching
|
||
|
|
if let Some(prev) = self.selected_index {
|
||
|
|
if prev != idx {
|
||
|
|
self.apply_draft();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let obj = &self.objects[idx];
|
||
|
|
self.draft = DraftState::from_object(obj);
|
||
|
|
self.selected_index = Some(idx);
|
||
|
|
self.redraw(); // removes the old rasterized version
|
||
|
|
}
|
||
|
|
|
||
|
|
fn deselect(&mut self) {
|
||
|
|
self.apply_draft();
|
||
|
|
self.selected_index = None;
|
||
|
|
self.redraw();
|
||
|
|
}
|
||
|
|
|
||
|
|
fn canvas_point(&self, screen: Pos2, rect: egui::Rect, scale: f32) -> Pos2 {
|
||
|
|
Pos2::new(
|
||
|
|
(screen.x - rect.min.x) / scale,
|
||
|
|
(screen.y - rect.min.y) / scale,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
impl eframe::App for App {
|
||
|
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||
|
|
let ctx = ui.ctx().clone();
|
||
|
|
let mut draft_changed = false;
|
||
|
|
if self.dirty {
|
||
|
|
let image = ColorImage::from_rgba_unmultiplied(
|
||
|
|
[self.canvas_size[0] as usize, self.canvas_size[1] as usize],
|
||
|
|
&self.canvas_pixels,
|
||
|
|
);
|
||
|
|
if let Some(tex) = &mut self.texture {
|
||
|
|
tex.set(image, egui::TextureOptions::NEAREST);
|
||
|
|
} else {
|
||
|
|
self.texture =
|
||
|
|
Some(ctx.load_texture("canvas", image, egui::TextureOptions::NEAREST));
|
||
|
|
}
|
||
|
|
self.dirty = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut fonts = self.renderer.get_fonts();
|
||
|
|
fonts.sort();
|
||
|
|
|
||
|
|
// ── Left panel ──────────────────────────────────────────────
|
||
|
|
egui::Panel::left("panel").show(&ctx, |ui| {
|
||
|
|
ui.heading("Text Editor");
|
||
|
|
if !self.font_loaded {
|
||
|
|
ui.colored_label(Color32::RED, "No fonts found.");
|
||
|
|
}
|
||
|
|
|
||
|
|
ui.separator();
|
||
|
|
if let Some(idx) = self.selected_index {
|
||
|
|
ui.strong(format!("Editing object #{} — click Done when finished", idx));
|
||
|
|
} else {
|
||
|
|
ui.label("Click canvas to add text.");
|
||
|
|
}
|
||
|
|
|
||
|
|
ui.separator();
|
||
|
|
ui.label("Text");
|
||
|
|
if ui.text_edit_singleline(&mut self.draft.text).changed() {
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
ui.label("Font family");
|
||
|
|
egui::ComboBox::from_id_source("font_select")
|
||
|
|
.selected_text(&self.draft.base_font)
|
||
|
|
.show_ui(ui, |ui| {
|
||
|
|
let mut seen = std::collections::HashSet::new();
|
||
|
|
for f in &fonts {
|
||
|
|
let base = extract_base_font(f);
|
||
|
|
if seen.insert(base.clone()) {
|
||
|
|
if ui.selectable_value(
|
||
|
|
&mut self.draft.base_font,
|
||
|
|
base.clone(),
|
||
|
|
&base,
|
||
|
|
).changed() {
|
||
|
|
let b = self.draft.base_font.clone();
|
||
|
|
self.draft.set_base_font(&b, &self.renderer);
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Bold / Italic toggles
|
||
|
|
ui.horizontal(|ui| {
|
||
|
|
let bold_btn = ui.add(egui::Button::new("B").selected(self.draft.bold));
|
||
|
|
if bold_btn.clicked() {
|
||
|
|
self.draft.bold = !self.draft.bold;
|
||
|
|
self.draft.resolve_font(&self.renderer);
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
let italic_btn = ui.add(egui::Button::new("I").selected(self.draft.italic));
|
||
|
|
if italic_btn.clicked() {
|
||
|
|
self.draft.italic = !self.draft.italic;
|
||
|
|
self.draft.resolve_font(&self.renderer);
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
ui.label("Size");
|
||
|
|
if ui.add(egui::Slider::new(&mut self.draft.size, 8.0..=300.0)).changed() {
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut rgba = [
|
||
|
|
self.draft.color[0] as f32 / 255.,
|
||
|
|
self.draft.color[1] as f32 / 255.,
|
||
|
|
self.draft.color[2] as f32 / 255.,
|
||
|
|
self.draft.color[3] as f32 / 255.,
|
||
|
|
];
|
||
|
|
if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() {
|
||
|
|
self.draft.color = [
|
||
|
|
(rgba[0] * 255.) as u8,
|
||
|
|
(rgba[1] * 255.) as u8,
|
||
|
|
(rgba[2] * 255.) as u8,
|
||
|
|
(rgba[3] * 255.) as u8,
|
||
|
|
];
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ui.checkbox(&mut self.anti_alias, "Anti-aliasing").changed() {
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if self.selected_index.is_some() {
|
||
|
|
ui.separator();
|
||
|
|
let done = ui.add_sized(
|
||
|
|
egui::vec2(ui.available_width(), 32.0),
|
||
|
|
egui::Button::new(
|
||
|
|
egui::RichText::new("Done").color(Color32::WHITE),
|
||
|
|
)
|
||
|
|
.fill(Color32::from_rgb(34, 139, 34))
|
||
|
|
.rounding(4.0),
|
||
|
|
);
|
||
|
|
if done.clicked() {
|
||
|
|
self.deselect();
|
||
|
|
}
|
||
|
|
|
||
|
|
ui.horizontal(|ui| {
|
||
|
|
if ui.button("Delete").clicked() {
|
||
|
|
if let Some(idx) = self.selected_index {
|
||
|
|
self.objects.remove(idx);
|
||
|
|
self.selected_index = None;
|
||
|
|
self.redraw();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
ui.separator();
|
||
|
|
if ui.button("Clear All").clicked() {
|
||
|
|
self.objects.clear();
|
||
|
|
self.selected_index = None;
|
||
|
|
self.redraw();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Central canvas ──────────────────────────────────────────
|
||
|
|
egui::CentralPanel::default().show(&ctx, |ui| {
|
||
|
|
let available = ui.available_size_before_wrap();
|
||
|
|
let scale = (available.x / self.canvas_size[0] as f32)
|
||
|
|
.min(available.y / self.canvas_size[1] as f32);
|
||
|
|
let disp_w = self.canvas_size[0] as f32 * scale;
|
||
|
|
let disp_h = self.canvas_size[1] as f32 * scale;
|
||
|
|
|
||
|
|
let sense = egui::Sense::click_and_drag();
|
||
|
|
let (rect, resp) = ui.allocate_exact_size(egui::vec2(disp_w, disp_h), sense);
|
||
|
|
|
||
|
|
// Click → select existing or create new
|
||
|
|
if resp.clicked() {
|
||
|
|
if let Some(pos) = resp.interact_pointer_pos() {
|
||
|
|
let cp = self.canvas_point(pos, rect, scale);
|
||
|
|
let hit = self.objects.iter().rposition(|o| o.contains(cp));
|
||
|
|
if let Some(hit_idx) = hit {
|
||
|
|
self.select_object(hit_idx);
|
||
|
|
} else {
|
||
|
|
// Create new object with current draft settings
|
||
|
|
if let Ok((_pixels, tw, th, _, _, _, _)) = self.renderer.rasterize_text(
|
||
|
|
&self.draft.text,
|
||
|
|
&self.draft.font,
|
||
|
|
self.draft.size,
|
||
|
|
self.draft.color,
|
||
|
|
0.0,
|
||
|
|
0.0,
|
||
|
|
0.0,
|
||
|
|
hcie_protocol::tools::TextAlignment::Left,
|
||
|
|
hcie_protocol::tools::TextOrientation::Horizontal,
|
||
|
|
&[],
|
||
|
|
self.anti_alias,
|
||
|
|
) {
|
||
|
|
// If something was already selected, apply it first
|
||
|
|
if self.selected_index.is_some() {
|
||
|
|
self.deselect();
|
||
|
|
}
|
||
|
|
let idx = self.objects.len();
|
||
|
|
self.objects.push(TextObject {
|
||
|
|
text: self.draft.text.clone(),
|
||
|
|
font: self.draft.font.clone(),
|
||
|
|
size: self.draft.size,
|
||
|
|
color: self.draft.color,
|
||
|
|
pos: cp,
|
||
|
|
w: tw,
|
||
|
|
h: th,
|
||
|
|
});
|
||
|
|
self.draft = DraftState::from_object(&self.objects[idx]);
|
||
|
|
self.selected_index = Some(idx);
|
||
|
|
self.redraw();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Drag → move selected object
|
||
|
|
if resp.dragged_by(egui::PointerButton::Primary) {
|
||
|
|
if let Some(idx) = self.selected_index {
|
||
|
|
if let Some(pos) = resp.interact_pointer_pos() {
|
||
|
|
let cp = self.canvas_point(pos, rect, scale);
|
||
|
|
self.objects[idx].pos = cp;
|
||
|
|
self.redraw();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Paint canvas texture (committed objects only)
|
||
|
|
if let Some(tex) = &self.texture {
|
||
|
|
ui.painter().image(
|
||
|
|
tex.id(),
|
||
|
|
rect,
|
||
|
|
egui::Rect::from_min_max(egui::pos2(0., 0.), egui::pos2(1., 1.)),
|
||
|
|
Color32::WHITE,
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
ui.painter().rect_filled(rect, 0.0, Color32::GRAY);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Selection rectangle + inline text edit on canvas
|
||
|
|
if let Some(idx) = self.selected_index {
|
||
|
|
if idx < self.objects.len() {
|
||
|
|
let obj = &self.objects[idx];
|
||
|
|
let b = obj.bounds();
|
||
|
|
let screen_rect = egui::Rect::from_min_size(
|
||
|
|
rect.min + egui::vec2(b.min.x * scale, b.min.y * scale),
|
||
|
|
egui::vec2(b.width() * scale, b.height() * scale),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Selection border
|
||
|
|
ui.painter()
|
||
|
|
.rect_stroke(screen_rect, 0.0, egui::Stroke::new(2.0, Color32::BLUE), egui::StrokeKind::Inside);
|
||
|
|
|
||
|
|
// Inline text edit directly on canvas (transparent — rasterized text shows through)
|
||
|
|
let edit_width = (rect.max.x - screen_rect.min.x - 10.0).max(400.0);
|
||
|
|
let edit_rect = egui::Rect::from_min_size(
|
||
|
|
screen_rect.min,
|
||
|
|
egui::vec2(edit_width, screen_rect.height().max(self.draft.size * scale * 1.2)),
|
||
|
|
);
|
||
|
|
|
||
|
|
ui.allocate_ui_at_rect(edit_rect, |ui| {
|
||
|
|
ui.visuals_mut().extreme_bg_color = Color32::TRANSPARENT;
|
||
|
|
let text_resp = ui.add(
|
||
|
|
egui::TextEdit::singleline(&mut self.draft.text)
|
||
|
|
.font(egui::FontId::monospace(self.draft.size * scale))
|
||
|
|
.text_color(Color32::TRANSPARENT)
|
||
|
|
.frame(egui::Frame::NONE),
|
||
|
|
);
|
||
|
|
if text_resp.changed() {
|
||
|
|
draft_changed = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if draft_changed {
|
||
|
|
self.apply_draft();
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.request_repaint();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn main() -> eframe::Result<()> {
|
||
|
|
eframe::run_native(
|
||
|
|
"HCIE Text Engine — Live Editor",
|
||
|
|
eframe::NativeOptions {
|
||
|
|
viewport: egui::ViewportBuilder::default().with_inner_size([1200., 800.]),
|
||
|
|
..Default::default()
|
||
|
|
},
|
||
|
|
Box::new(|cc| Ok(Box::new(App::new(cc)))),
|
||
|
|
)
|
||
|
|
}
|