Add Rust port

This commit is contained in:
darktohka
2026-03-17 03:10:07 +02:00
parent 64de0ade31
commit f8a1dde721
38 changed files with 4806 additions and 1 deletions
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "clean_flash_ui"
version = "34.0.0"
edition = "2021"
[dependencies]
ab_glyph = { workspace = true }
image = { workspace = true }
minifb = { workspace = true }
windows-sys = { workspace = true }
+106
View File
@@ -0,0 +1,106 @@
use ab_glyph::{point, Font, FontRef, ScaleFont};
use crate::renderer::Renderer;
/// Manages loaded fonts and provides text measurement / rendering.
pub struct FontManager {
regular: FontRef<'static>,
}
impl FontManager {
/// Create a `FontManager` using the bundled Liberation Sans font.
pub fn new() -> Self {
let regular = FontRef::try_from_slice(include_bytes!(
"../../../resources/liberation-sans.regular.ttf"
))
.expect("Failed to parse bundled Liberation Sans font");
Self { regular }
}
/// Measure the width (in pixels) of `text` at the given `size` (in px).
pub fn measure_text(&self, text: &str, size: f32) -> (f32, f32) {
let scaled = self.regular.as_scaled(size);
let mut width: f32 = 0.0;
let height = scaled.height();
let mut last_glyph_id = None;
for ch in text.chars() {
let glyph_id = scaled.glyph_id(ch);
if let Some(prev) = last_glyph_id {
width += scaled.kern(prev, glyph_id);
}
width += scaled.h_advance(glyph_id);
last_glyph_id = Some(glyph_id);
}
(width, height)
}
/// Draw `text` onto the renderer at (x, y) with the given pixel size and colour.
pub fn draw_text(
&self,
renderer: &mut Renderer,
x: i32,
y: i32,
text: &str,
size: f32,
color: u32,
) {
let scaled = self.regular.as_scaled(size);
let ascent = scaled.ascent();
let mut cursor_x: f32 = 0.0;
let mut last_glyph_id = None;
for ch in text.chars() {
let glyph_id = scaled.glyph_id(ch);
if let Some(prev) = last_glyph_id {
cursor_x += scaled.kern(prev, glyph_id);
}
let glyph = glyph_id.with_scale_and_position(
size,
point(x as f32 + cursor_x, y as f32 + ascent),
);
if let Some(outlined) = self.regular.outline_glyph(glyph) {
let bounds = outlined.px_bounds();
outlined.draw(|gx, gy, coverage| {
let px = bounds.min.x as i32 + gx as i32;
let py = bounds.min.y as i32 + gy as i32;
let alpha = (coverage * 255.0) as u8;
if alpha > 0 {
renderer.blend_pixel(px, py, color, alpha);
}
});
}
cursor_x += scaled.h_advance(glyph_id);
last_glyph_id = Some(glyph_id);
}
}
/// Draw multiline text, splitting on '\n'. Returns total height drawn.
pub fn draw_text_multiline(
&self,
renderer: &mut Renderer,
x: i32,
y: i32,
text: &str,
size: f32,
color: u32,
line_spacing: f32,
) -> f32 {
let scaled = self.regular.as_scaled(size);
let line_height = scaled.height() + line_spacing;
let mut cy = y as f32;
for line in text.split('\n') {
self.draw_text(renderer, x, cy as i32, line, size, color);
cy += line_height;
}
cy - y as f32
}
}
+28
View File
@@ -0,0 +1,28 @@
pub mod font;
pub mod renderer;
pub mod widgets;
pub use font::FontManager;
pub use renderer::Renderer;
/// Set the window icon from the icon resource already embedded in the binary
/// by `winresource` (resource ID 1). No-op on non-Windows platforms.
pub fn set_window_icon(window: &minifb::Window) {
#[cfg(target_os = "windows")]
unsafe {
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::WindowsAndMessaging::{
LoadImageW, SendMessageW, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE,
WM_SETICON,
};
let hwnd = window.get_window_handle(); // *mut c_void
let hmodule = GetModuleHandleW(std::ptr::null());
// MAKEINTRESOURCEW(1): load the icon embedded by winresource as resource ID 1.
let icon = LoadImageW(hmodule, 1 as *const u16, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE);
if !icon.is_null() {
SendMessageW(hwnd, WM_SETICON, ICON_BIG as usize, icon as isize);
SendMessageW(hwnd, WM_SETICON, ICON_SMALL as usize, icon as isize);
}
}
}
+183
View File
@@ -0,0 +1,183 @@
/// Software renderer operating on a `Vec<u32>` pixel buffer (0xAA_RR_GG_BB).
/// All drawing is done in-memory; the buffer is presented via minifb.
pub struct Renderer {
pub width: usize,
pub height: usize,
pub buffer: Vec<u32>,
}
impl Renderer {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
buffer: vec![0; width * height],
}
}
/// Pack r, g, b into the minifb pixel format (0x00RRGGBB).
#[inline]
pub const fn rgb(r: u8, g: u8, b: u8) -> u32 {
((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}
/// Clear the entire buffer to a single colour.
pub fn clear(&mut self, color: u32) {
self.buffer.fill(color);
}
/// Set a single pixel (bounds-checked).
#[inline]
pub fn set_pixel(&mut self, x: i32, y: i32, color: u32) {
if x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height {
self.buffer[y as usize * self.width + x as usize] = color;
}
}
/// Alpha-blend a single pixel. `alpha` is 0..=255.
#[inline]
pub fn blend_pixel(&mut self, x: i32, y: i32, color: u32, alpha: u8) {
if x < 0 || y < 0 || (x as usize) >= self.width || (y as usize) >= self.height {
return;
}
let idx = y as usize * self.width + x as usize;
let dst = self.buffer[idx];
self.buffer[idx] = alpha_blend(dst, color, alpha);
}
/// Fill a solid rectangle.
pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: u32) {
let x0 = x.max(0) as usize;
let y0 = y.max(0) as usize;
let x1 = ((x + w) as usize).min(self.width);
let y1 = ((y + h) as usize).min(self.height);
for row in y0..y1 {
let start = row * self.width + x0;
let end = row * self.width + x1;
self.buffer[start..end].fill(color);
}
}
/// Draw a 1px rectangle outline.
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: u32) {
// Top / bottom
for dx in 0..w {
self.set_pixel(x + dx, y, color);
self.set_pixel(x + dx, y + h - 1, color);
}
// Left / right
for dy in 0..h {
self.set_pixel(x, y + dy, color);
self.set_pixel(x + w - 1, y + dy, color);
}
}
/// Fill a rectangle with a vertical linear gradient from `color1` (top) to `color2` (bottom).
pub fn fill_gradient_v(&mut self, x: i32, y: i32, w: i32, h: i32, color1: u32, color2: u32) {
if h <= 0 {
return;
}
let (r1, g1, b1) = unpack(color1);
let (r2, g2, b2) = unpack(color2);
for dy in 0..h {
let t = dy as f32 / (h - 1).max(1) as f32;
let r = lerp_u8(r1, r2, t);
let g = lerp_u8(g1, g2, t);
let b = lerp_u8(b1, b2, t);
let c = Self::rgb(r, g, b);
self.fill_rect(x, y + dy, w, 1, c);
}
}
/// Fill a rectangle with a horizontal linear gradient.
pub fn fill_gradient_h(&mut self, x: i32, y: i32, w: i32, h: i32, color1: u32, color2: u32) {
if w <= 0 {
return;
}
let (r1, g1, b1) = unpack(color1);
let (r2, g2, b2) = unpack(color2);
for dx in 0..w {
let t = dx as f32 / (w - 1).max(1) as f32;
let r = lerp_u8(r1, r2, t);
let g = lerp_u8(g1, g2, t);
let b = lerp_u8(b1, b2, t);
let c = Self::rgb(r, g, b);
self.fill_rect(x + dx, y, 1, h, c);
}
}
/// Draw an RGBA image onto the framebuffer at (x, y).
pub fn draw_image(&mut self, x: i32, y: i32, img: &RgbaImage) {
for iy in 0..img.height as i32 {
for ix in 0..img.width as i32 {
let idx = (iy as usize * img.width + ix as usize) * 4;
let r = img.data[idx];
let g = img.data[idx + 1];
let b = img.data[idx + 2];
let a = img.data[idx + 3];
if a == 255 {
self.set_pixel(x + ix, y + iy, Self::rgb(r, g, b));
} else if a > 0 {
self.blend_pixel(x + ix, y + iy, Self::rgb(r, g, b), a);
}
}
}
}
}
/// Simple RGBA image stored as raw bytes.
pub struct RgbaImage {
pub width: usize,
pub height: usize,
pub data: Vec<u8>, // RGBA, row-major
}
impl RgbaImage {
/// Load a PNG from embedded bytes.
pub fn from_png_bytes(bytes: &[u8]) -> Self {
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
.expect("Failed to decode PNG")
.to_rgba8();
Self {
width: img.width() as usize,
height: img.height() as usize,
data: img.into_raw(),
}
}
/// Create an empty (transparent) image.
pub fn empty() -> Self {
Self {
width: 0,
height: 0,
data: Vec::new(),
}
}
}
// ---- helpers ----
#[inline]
fn unpack(c: u32) -> (u8, u8, u8) {
(((c >> 16) & 0xFF) as u8, ((c >> 8) & 0xFF) as u8, (c & 0xFF) as u8)
}
#[inline]
fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
(a as f32 + (b as f32 - a as f32) * t).round() as u8
}
#[inline]
fn alpha_blend(dst: u32, src: u32, alpha: u8) -> u32 {
let (sr, sg, sb) = unpack(src);
let (dr, dg, db) = unpack(dst);
let a = alpha as u16;
let inv = 255 - a;
let r = ((sr as u16 * a + dr as u16 * inv) / 255) as u8;
let g = ((sg as u16 * a + dg as u16 * inv) / 255) as u8;
let b = ((sb as u16 * a + db as u16 * inv) / 255) as u8;
Renderer::rgb(r, g, b)
}
@@ -0,0 +1,97 @@
use super::Rect;
use crate::font::FontManager;
use crate::renderer::Renderer;
/// A gradient button matching the C# GradientButton control.
pub struct GradientButton {
pub rect: Rect,
pub text: String,
pub color1: u32,
pub color2: u32,
pub back_color: u32,
pub fore_color: u32,
pub hover_alpha: f64,
pub disable_alpha: f64,
pub enabled: bool,
pub visible: bool,
pub hovered: bool,
pub pressed: bool,
}
impl GradientButton {
pub fn new(x: i32, y: i32, w: i32, h: i32, text: &str) -> Self {
Self {
rect: Rect::new(x, y, w, h),
text: text.to_string(),
color1: Renderer::rgb(118, 118, 118),
color2: Renderer::rgb(81, 81, 81),
back_color: Renderer::rgb(0, 0, 0),
fore_color: Renderer::rgb(227, 227, 227),
hover_alpha: 0.875,
disable_alpha: 0.644,
enabled: true,
visible: true,
hovered: false,
pressed: false,
}
}
/// Update hover / pressed state from mouse position and button state.
pub fn update(&mut self, mx: i32, my: i32, mouse_down: bool) {
if !self.visible || !self.enabled {
self.hovered = false;
self.pressed = false;
return;
}
self.hovered = self.rect.contains(mx, my);
self.pressed = self.hovered && mouse_down;
}
/// Returns true if mouse was just released inside this button.
pub fn clicked(&self, mx: i32, my: i32, mouse_released: bool) -> bool {
self.visible && self.enabled && self.rect.contains(mx, my) && mouse_released
}
pub fn draw(&self, renderer: &mut Renderer, fonts: &FontManager) {
if !self.visible {
return;
}
let (mut c1, mut c2, mut bg, mut fg) = (self.color1, self.color2, self.back_color, self.fore_color);
if !self.enabled {
c1 = dim_color(c1, self.disable_alpha);
c2 = dim_color(c2, self.disable_alpha);
bg = dim_color(bg, self.disable_alpha);
fg = dim_color(fg, self.disable_alpha);
} else if self.pressed {
c1 = dim_color(c1, self.hover_alpha);
c2 = dim_color(c2, self.hover_alpha);
} else if !self.hovered {
c1 = dim_color(c1, self.hover_alpha);
c2 = dim_color(c2, self.hover_alpha);
}
let r = self.rect;
renderer.fill_gradient_v(r.x, r.y, r.w, r.h, c1, c2);
renderer.draw_rect(r.x, r.y, r.w, r.h, bg);
// Measure text to centre it.
let font_size = 13.0;
let (tw, th) = fonts.measure_text(&self.text, font_size);
let tx = r.x + ((r.w as f32 - tw) / 2.0) as i32;
let ty = r.y + ((r.h as f32 - th) / 2.0) as i32;
// Shadow.
fonts.draw_text(renderer, tx + 1, ty + 1, &self.text, font_size, bg);
// Foreground.
fonts.draw_text(renderer, tx, ty, &self.text, font_size, fg);
}
}
fn dim_color(c: u32, alpha: f64) -> u32 {
let r = (((c >> 16) & 0xFF) as f64 * alpha) as u8;
let g = (((c >> 8) & 0xFF) as f64 * alpha) as u8;
let b = ((c & 0xFF) as f64 * alpha) as u8;
Renderer::rgb(r, g, b)
}
@@ -0,0 +1,91 @@
use super::Rect;
use crate::renderer::{Renderer, RgbaImage};
/// An image-based checkbox matching the C# ImageCheckBox control.
pub struct ImageCheckBox {
pub rect: Rect,
pub checked: bool,
pub enabled: bool,
pub visible: bool,
}
impl ImageCheckBox {
pub fn new(x: i32, y: i32) -> Self {
Self {
rect: Rect::new(x, y, 21, 21),
checked: true,
enabled: true,
visible: true,
}
}
pub fn toggle_if_clicked(&mut self, mx: i32, my: i32, mouse_released: bool) -> bool {
if self.visible && self.enabled && self.rect.contains(mx, my) && mouse_released {
self.checked = !self.checked;
true
} else {
false
}
}
pub fn draw(
&self,
renderer: &mut Renderer,
checked_img: &RgbaImage,
unchecked_img: &RgbaImage,
) {
if !self.visible {
return;
}
let img = if self.checked {
checked_img
} else {
unchecked_img
};
if img.width > 0 && img.height > 0 {
renderer.draw_image(self.rect.x, self.rect.y, img);
} else {
// Fallback: draw a simple square.
let bg = if self.checked {
Renderer::rgb(97, 147, 232)
} else {
Renderer::rgb(80, 80, 80)
};
renderer.fill_rect(self.rect.x, self.rect.y, self.rect.w, self.rect.h, bg);
renderer.draw_rect(
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
Renderer::rgb(160, 160, 160),
);
if self.checked {
// Draw a simple checkmark.
let cx = self.rect.x + 5;
let cy = self.rect.y + 10;
for i in 0..4 {
renderer.set_pixel(cx + i, cy + i, Renderer::rgb(255, 255, 255));
renderer.set_pixel(cx + i, cy + i + 1, Renderer::rgb(255, 255, 255));
}
for i in 0..8 {
renderer.set_pixel(cx + 3 + i, cy + 3 - i, Renderer::rgb(255, 255, 255));
renderer.set_pixel(cx + 3 + i, cy + 4 - i, Renderer::rgb(255, 255, 255));
}
}
}
if !self.enabled {
// Dim overlay.
for dy in 0..self.rect.h {
for dx in 0..self.rect.w {
renderer.blend_pixel(
self.rect.x + dx,
self.rect.y + dy,
Renderer::rgb(50, 51, 51),
100,
);
}
}
}
}
}
@@ -0,0 +1,51 @@
use super::Rect;
use crate::font::FontManager;
use crate::renderer::Renderer;
/// Simple static label for drawing text.
pub struct Label {
pub rect: Rect,
pub text: String,
pub color: u32,
pub font_size: f32,
pub visible: bool,
}
impl Label {
pub fn new(x: i32, y: i32, text: &str, font_size: f32) -> Self {
Self {
rect: Rect::new(x, y, 0, 0),
text: text.to_string(),
color: Renderer::rgb(245, 245, 245),
font_size,
visible: true,
}
}
pub fn draw(&self, renderer: &mut Renderer, fonts: &FontManager) {
if !self.visible || self.text.is_empty() {
return;
}
fonts.draw_text_multiline(
renderer,
self.rect.x,
self.rect.y,
&self.text,
self.font_size,
self.color,
2.0,
);
}
/// Check if a click at (mx, my) is within a rough bounding box of the label text.
pub fn clicked(&self, mx: i32, my: i32, mouse_released: bool, fonts: &FontManager) -> bool {
if !self.visible || !mouse_released {
return false;
}
let (tw, _th) = fonts.measure_text(&self.text, self.font_size);
let lines = self.text.lines().count().max(1);
let approx_h = (self.font_size * lines as f32 + 2.0 * lines as f32) as i32;
let r = Rect::new(self.rect.x, self.rect.y, tw as i32 + 5, approx_h);
r.contains(mx, my)
}
}
@@ -0,0 +1,23 @@
pub mod button;
pub mod checkbox;
pub mod label;
pub mod progress_bar;
/// A rectangle region on screen.
#[derive(Clone, Copy, Debug, Default)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
impl Rect {
pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
Self { x, y, w, h }
}
pub fn contains(&self, px: i32, py: i32) -> bool {
px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h
}
}
@@ -0,0 +1,68 @@
use super::Rect;
use crate::renderer::Renderer;
/// A smooth gradient progress bar matching the C# SmoothProgressBar control.
pub struct ProgressBar {
pub rect: Rect,
pub minimum: i32,
pub maximum: i32,
pub value: i32,
pub color1: u32,
pub color2: u32,
pub visible: bool,
}
impl ProgressBar {
pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
Self {
rect: Rect::new(x, y, w, h),
minimum: 0,
maximum: 100,
value: 0,
color1: Renderer::rgb(97, 147, 232),
color2: Renderer::rgb(28, 99, 232),
visible: true,
}
}
pub fn draw(&self, renderer: &mut Renderer) {
if !self.visible {
return;
}
let range = (self.maximum - self.minimum).max(1) as f32;
let percent = (self.value - self.minimum) as f32 / range;
let fill_w = (self.rect.w as f32 * percent) as i32;
if fill_w > 0 {
renderer.fill_gradient_h(
self.rect.x,
self.rect.y,
fill_w,
self.rect.h,
self.color1,
self.color2,
);
}
// 3-D border.
let r = self.rect;
let dark = Renderer::rgb(105, 105, 105);
let light = Renderer::rgb(255, 255, 255);
// Top
for dx in 0..r.w {
renderer.set_pixel(r.x + dx, r.y, dark);
}
// Left
for dy in 0..r.h {
renderer.set_pixel(r.x, r.y + dy, dark);
}
// Bottom
for dx in 0..r.w {
renderer.set_pixel(r.x + dx, r.y + r.h - 1, light);
}
// Right
for dy in 0..r.h {
renderer.set_pixel(r.x + r.w - 1, r.y + dy, light);
}
}
}