init
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
#![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)))),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user