Files
hcie-rust-v3.05/hcie-text/examples/text_test.rs
T
2026-07-09 02:59:53 +03:00

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)))),
)
}