Skip to main content

miniwrap/
lib.rs

1//!A wrapper around [minifb] that makes managing windows as simple as possible, with hexidecimal RGB rather than raw u32.
2//!
3//!Window elements like buffers and dimensions are linked together in the [WindowContainer] struct. To keep the window alive, call update() from within a loop. This has a built-in graceful exit by pressing ESC.
4//!
5//!```rust
6//!let window = window!(?500, 500, "Window", "FFFFFF");
7//!
8//!loop {
9//!    window.update();
10//!}
11//!```
12//! 
13//!Heres an example of a UV map generated from pixel coordinates:
14//!```rust
15//!let window = window!(?500, 500, "Window", "FFFFFF");
16//!
17//!//iterates with the value and position
18//!for (_, pos) in window.iter() {
19//!    let r = to_hex2(pos.0 as u8).unwrap();
20//!    let g = to_hex2(pos.1 as u8).unwrap();
21//!    let string = format!("{r}{g}00");
22//!
23//!    window.set(pos, &string)?;
24//!}
25//!
26//!//pressing escape will close the window
27//!loop {
28//!    window.update();
29//!}
30//!```
31//! 
32//! 
33//! 
34//!Note with the hexidecimal conversions: functions that take hexidecimal will take a &str for convenience, but functions that return hexidecimal will return a [String].
35//! 
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50//use minifb;
51use std::{process, ops::Range};
52fn from_u8_rgb(r: u8, g: u8, b: u8) -> u32 {
53    let (r, g, b) = (r as u32, g as u32, b as u32);
54    (r << 16) | (g << 8) | b
55}
56
57type Unit = Result<(), WinErr>;
58
59macro_rules! unit {
60    () => {
61        Ok(())
62    };
63}
64///Initializes a [WindowContainer]. Optional background color. Begin with ? to unwrap.
65#[macro_export] macro_rules! window {
66    ($width:expr, $height:expr, $name:tt) => {
67        WindowContainer::new($width, $height, $name, "FFFFFF")
68    };
69    (?$width:expr, $height:expr, $name:tt) => {
70        WindowContainer::new($width, $height, $name, "FFFFFF").unwrap()
71    };
72
73    ($width:expr, $height:expr, $name:tt, $color:tt) => {
74        WindowContainer::new($width, $height, $name, $color)
75    };
76    (?$width:expr, $height:expr, $name:tt, $color:tt) => {
77        WindowContainer::new($width, $height, $name, $color).unwrap()
78    };
79
80}
81
82///converts a 0F0F0F format hex string into a u32.
83pub fn hex_to_rgb(code:String) -> Result<u32, WinErr> {
84    if code.len() != 6 {return Err(WinErr::InvalidHexCode(code))}
85
86    let (r, gb) = code.split_at(2);
87    let (g, b) = gb.split_at(2);
88    
89    let out = from_u8_rgb(
90    from_hex8(r.to_string())? as u8,
91    from_hex8(g.to_string())? as u8,
92    from_hex8(b.to_string())? as u8
93    );
94
95    Ok(out)
96}
97
98
99
100fn hex2num_code(c: char) -> Result<u8, WinErr> {
101    c.to_digit(16)
102        .map(|n| n as u8)
103        .ok_or(WinErr::InvalidHexChar(c))
104}
105///Converts an (up to) 8-wide hexidecimal string to a u8.
106pub fn from_hex8(input:String) -> Result<u32, WinErr> {
107    if input.len() > 8 {return Err(WinErr::InvalidHexCode(input))}
108
109    let mut output = 0_u32;
110    
111    let iterator = input.chars().rev().enumerate();
112
113    for (i, c) in iterator {
114        let val = hex2num_code(c)? as u32;
115        output += val * 16_u32.pow(i as u32);
116    }
117
118    Ok(output)
119}
120fn num2hex_code(num: u8) -> Result<char, WinErr> {
121    match num {
122        0..=9 => Ok((num + 48) as char),
123        10..=15 => Ok((num + 55) as char),
124        _ => Err(WinErr::InvalidNumCode(num))
125    }
126}
127///Converts a u32 to a 6-wide hexidecimal string. Note that this includes leading zeros.
128pub fn to_hex6(num: u32) -> Result<String, WinErr> {
129    if num > 16777215 {return Err(WinErr::OverflowNum(num))}    
130
131    let mut hex_string = String::new();
132
133    for i in (0..6).rev() {
134        let shift = i * 4;
135        let hex_digit = ((num >> shift) & 0xF) as u8;
136        let hex_char = num2hex_code(hex_digit)?;
137
138        hex_string.push(hex_char);
139    }
140
141    Ok(hex_string)
142}
143///Converts a u8 to a 2-wide hexidecimal string, to concatenate into a RGB hex string. Note that this includes leading zero.
144pub fn to_hex2(num: u8) -> Result<String, WinErr> {
145    let mut hex_string = String::new();
146
147    for i in (0..2).rev() {
148        let shift = i * 4;
149        let hex_digit = (num >> shift) & 0xF;
150        let hex_char = num2hex_code(hex_digit)?;
151
152        hex_string.push(hex_char);
153    }
154
155    Ok(hex_string)
156}
157
158
159
160
161fn dist(p1:(usize, usize), p2:(usize, usize)) -> f64 {
162    let fp1 = (p1.0 as f64, p1.1 as f64);
163    let fp2 = (p2.0 as f64, p2.1 as f64);
164    ((fp2.0 - fp1.0).powi(2) + (fp2.1 - fp1.1).powi(2)).sqrt()
165
166}
167
168fn tupf64(tup:(usize, usize)) -> (f64, f64) {
169    (tup.0 as f64, tup.1 as f64)
170}
171
172fn perpendicular_line(slope: f64, midpoint: (f64, f64), length: f64) -> ((f64, f64), (f64, f64)) {
173    let m_perp = -1.0 / slope;
174    let dx = length / (2.0 * (1.0 + m_perp.powi(2)).sqrt());
175    let dy = m_perp * dx;
176    let start = (midpoint.0 - dx, midpoint.1 - dy);
177    let end = (midpoint.0 + dx, midpoint.1 + dy);
178    (start, end)
179}
180
181///Contains a Window, pixel buffer, and properties.
182pub struct WindowContainer {
183    buffer:Vec<u32>,
184    window:minifb::Window,
185
186    width:usize,
187    height:usize,
188    pub bg_color:u32,
189
190    length:usize,
191
192}
193
194impl WindowContainer {
195    ///Initalizes a new WindowContainer.
196    pub fn new(width:usize, height:usize, name:&str, color:&str) -> Result<Self, WinErr> {
197        let bg_color = hex_to_rgb(color.to_string())?;
198        if bg_color > 16777215 {return Err(WinErr::InvalidRGBValue(bg_color))}
199
200        let buffer = vec![bg_color; width * height];
201        let window = minifb::Window::new(name, width, height, minifb::WindowOptions::default()).unwrap();
202
203        let length = buffer.len();
204
205        Ok(WindowContainer {buffer, window, width, height, bg_color, length})
206    }
207
208    ///Updates the window with its pixel buffer. Pressing ESC will gracefully exit the program.
209    ///```rust
210    ///let mut window = window!(?500, 500, "Window", "FFFFFF");
211    ///
212    ///loop {//can close by pressing ESC
213    ///    window.update();
214    ///}
215    ///```
216    pub fn update(&mut self) -> Unit {
217        if self.window.is_key_down(minifb::Key::Escape) {process::exit(1)}
218
219
220        self.window.update_with_buffer(&self.buffer, self.width, self.height)?;
221        unit!()
222    }
223    ///clears the screen to the background color.
224    ///```rust
225    ///let mut window = window!(?500, 500, "Window", "FF00FF");
226    /// 
227    ///loop {
228    ///    window.clear();//clears to "FF00FF"
229    /// 
230    ///    //drawing code
231    /// 
232    ///    window.update();
233    ///}
234    ///```
235    pub fn clear(&mut self) {
236        self.buffer = self.buffer.iter().map(|_| self.bg_color).collect();
237    }
238
239    //inputs should be converted from &str -> String so that inputing params is easier
240    ///Returns the hexidecimal value of a pixel at a position.
241    pub fn get(&self, pos:(usize, usize)) -> Result<String, WinErr> {
242        if pos.0 >= self.width  {return Err(WinErr::InvalidPos(pos))}
243        if pos.1 >= self.height {return Err(WinErr::InvalidPos(pos))}
244
245        let i = (pos.1 * self.width) + pos.0;
246        let val = self.buffer[i];
247        
248        to_hex6(val)
249    }
250    ///Sets a pixel to a hexidecimal value.
251    pub fn set(&mut self, pos:(usize, usize), val:&str) -> Result<(), WinErr> {
252        if pos.0 >= self.width  {return Err(WinErr::InvalidPos(pos))}
253        if pos.1 >= self.height {return Err(WinErr::InvalidPos(pos))}
254
255        let i = (pos.1 * self.width) + pos.0;
256        self.buffer[i] = hex_to_rgb(val.to_string())?;
257
258        unit!()
259    }
260
261    ///returns an iterator over the pixel buffer, holding the hex value and position. Note that the iterator pulled from the buffer is no longer linked to the window, and modifying it will do nothing.
262    ///```rust
263    ///let window = window!(?500, 500, "Window", "FFFFFF");
264    ///
265    /////val:String, pos:(usize, usize)
266    ///for (val, pos) in window.iter() {
267    ///    let r = to_hex2(pos.0 as u8).unwrap();
268    ///    let g = to_hex2(pos.1 as u8).unwrap();
269    ///    let string = format!("{r}{g}00");
270    ///
271    ///    window.set(pos, &string)?;
272    ///}
273    ///
274    /////pressing escape will close the window
275    ///loop {
276    ///    window.update();
277    ///}
278    ///```
279    pub fn iter(&self) -> std::vec::IntoIter<(String, (usize, usize))> {
280        let mut table:Vec<(String, (usize, usize))> = vec![];
281        
282        for i in 0..self.length {
283            let string = to_hex6(self.buffer[i]).unwrap();
284
285            table.push((string, self.nth_to_pos(i)));
286        }
287
288        table.into_iter()
289    }
290
291
292    fn pos_to_nth(&self, pos:(usize, usize)) -> usize {
293        (pos.1 * self.width) + pos.0
294    }
295    fn nth_to_pos(&self, i:usize) -> (usize, usize) {
296        (i / self.width, i % self.width)
297    }
298
299    //drawing
300    ///Draws a circle at a given position with a radius and color.
301    pub fn circle(&mut self, pos:(usize, usize), r:usize, color:&str) -> Unit {
302        if pos.0 >= self.width  {return Err(WinErr::InvalidPos(pos))}
303        if pos.1 >= self.height {return Err(WinErr::InvalidPos(pos))}
304
305        let y_range = pos.1-r..pos.1+r;
306
307        let mut range_table: Vec<Range<usize>> = vec![];
308        
309        for i in y_range {
310            range_table.push((pos.0+(self.width*i))-r..(pos.0+(self.width*i))+r)
311        }
312
313        for range in range_table {
314            for i in range {
315                let loc = self.nth_to_pos(i);
316
317                if dist(loc, pos) < r as f64 {self.buffer[i] = hex_to_rgb(color.to_string())?}
318            }
319        }
320
321
322        unit!()
323    }
324    ///Draws a line between 2 points, with a thickness and color.
325    pub fn line(&mut self, p1:(usize, usize), p2:(usize, usize), t:f64, color:&str) -> Unit {
326        let fp1 = tupf64(p1);
327        let fp2 = tupf64(p2);
328
329        //let normal_p1 = (0.0, 0.0);
330        let normal = (fp2.0-fp1.0, fp2.1-fp1.1);
331        
332        let m = (fp2.1 - fp1.1) / (fp2.0 - fp1.0);
333        let p_line = perpendicular_line(m, fp1, t);
334        let p_normal = (p_line.1.0 - p_line.0.0, p_line.1.1 - p_line.0.1);
335
336        //this should make the step smaller the longer the line is
337        let step = (1.0/dist(p1, p2)) * 0.9999;
338        let p_step = 0.5/t;
339        let mut t = 0.0;
340        //println!("{step}");
341            while t <= 1.0 {
342                let point = ((normal.0*t)+fp1.0, (normal.1*t)+fp1.1);
343                let usize_p = (point.0 as usize, point.1 as usize);
344
345                let loc = self.pos_to_nth(usize_p);
346                
347
348                if usize_p.0 < self.width && usize_p.1 < self.height {
349                    self.buffer[loc] = hex_to_rgb(color.to_string())?;
350                }
351
352                let p_line = perpendicular_line(m, point, t);
353
354                let mut j = 0.0;
355                while j <= 1.0 {
356                    let p_point = ((p_normal.0*j) + p_line.0.0, (p_normal.1*j) + p_line.0.1);
357                    let usize_p_point = (p_point.0 as usize, p_point.1 as usize);
358
359                    let p_loc = self.pos_to_nth(usize_p_point);
360                    
361                    if usize_p_point.0 < self.width && usize_p_point.1 < self.height {
362                        self.buffer[p_loc] = hex_to_rgb(color.to_string())?;
363                    }
364
365                    j += p_step;
366                }
367
368                t += step;
369            }
370
371
372        unit!()
373    }
374
375}
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn it_works() {
409        //const escape = UpdateOptions::escape;
410        let mut window = window!(?255, 255, "Window", "FF00FF");
411        println!("{:?}", from_hex8(String::from("FFFFFF")));
412        loop {
413            window.update();
414        }
415    }
416    #[test]
417    fn uv_map() -> Unit {
418        let mut window = window!(?500, 500, "Window", "FFFFFF");
419
420        for (_val, pos) in window.iter() {
421            let r = to_hex2(pos.0 as u8).unwrap();
422            let g = to_hex2(pos.1 as u8).unwrap();
423            let string = format!("{r}{g}00");
424            //println!("{string}");
425            window.set(pos, &string)?;
426        }
427
428        loop {
429            window.update();
430        }
431
432        unit!()
433    }
434    
435    #[test]
436    fn shapes() -> Unit {
437        let mut window = WindowContainer::new(1000, 1000, "Window", "FFFFFF").unwrap();
438
439        window.circle((100, 100), 50, "CC00FF");
440        
441        window.line((800, 1000), (800, 0), 50.0, "FF0000");
442        window.line((200, 200), (150, 230), 10.0, "0000FF");
443        //window.line((10, 170), (3000, 170), 3.0, "FF00FF");
444        
445        loop {
446            window.update();
447        }
448
449        unit!()
450    }
451}
452
453///Errors concerning the WindowContainer.
454#[derive(Debug)]
455pub enum WinErr {
456    MinifbError(minifb::Error),
457
458    InvalidHexCode(String),
459    InvalidHexChar(char),
460    InvalidNumCode(u8),
461    OverflowNum(u32),
462    InvalidRGBValue(u32),
463
464
465    InvalidIndex(usize),
466    InvalidPos((usize, usize)),
467}
468
469impl From<minifb::Error> for WinErr {
470    fn from(cause:minifb::Error) -> Self {
471        WinErr::MinifbError(cause)
472    }
473}
474