mirror of
https://gitlab.com/cleanflash/installer.git
synced 2026-08-13 02:41:38 +08:00
Add Rust port
This commit is contained in:
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user