From bee1f796ee3a64dc41c6af213bb45858f5f8a64c Mon Sep 17 00:00:00 2001 From: bunnie Date: Mon, 1 Mar 2021 01:49:22 +0800 Subject: [PATCH] add rounded rectangle drawing --- services/gam/src/lib.rs | 7 +- services/gam/src/main.rs | 60 ++--- services/gam/src/status.rs | 7 +- services/graphics-server/src/api.rs | 4 +- services/graphics-server/src/api/shapes.rs | 37 ++- services/graphics-server/src/api/text.rs | 62 ++--- services/graphics-server/src/lib.rs | 30 ++- services/graphics-server/src/main.rs | 133 +++++++++- services/graphics-server/src/op.rs | 286 +++++++++++++++++++-- xtask/src/main.rs | 4 +- 10 files changed, 497 insertions(+), 133 deletions(-) diff --git a/services/gam/src/lib.rs b/services/gam/src/lib.rs index 298010fa1..61bab93fa 100644 --- a/services/gam/src/lib.rs +++ b/services/gam/src/lib.rs @@ -14,12 +14,7 @@ use log::{error, info}; /// the GAM first has to check that the textview is allowed to be updated, and then it will decide when /// the actual screen update is allowed pub fn post_textview(gam_cid: xous::CID, tv: &mut TextView) -> Result<(), xous::Error> { - // for testing only tv.set_op(TextOp::Render); - tv.cursor.pt.x = 37; - tv.cursor.pt.y = 5; - - // info!("tv before lend: {:?}", tv); let mut rkyv_tv = api::Opcode::RenderTextView(*tv); let mut writer = rkyv::ArchiveBuffer::new(xous::XousBuffer::new(4096)); let pos = writer.archive(&rkyv_tv).expect("couldn't archive textview"); @@ -31,7 +26,7 @@ pub fn post_textview(gam_cid: xous::CID, tv: &mut TextView) -> Result<(), xous:: let returned = unsafe { rkyv::archived_value::(xous_buffer.as_ref(), pos)}; if let rkyv::Archived::::RenderTextView(result) = returned { let tvr: TextView = result.unarchive(); - tv.set_computed_bounds(tvr.get_bounds_computed()); + tv.bounds_computed = tvr.bounds_computed; tv.cursor = tvr.cursor; } else { let tvr = returned.unarchive(); diff --git a/services/gam/src/main.rs b/services/gam/src/main.rs index 4b6af3d14..7ccccc9c2 100644 --- a/services/gam/src/main.rs +++ b/services/gam/src/main.rs @@ -107,41 +107,6 @@ fn add_chat_layout(gfx_conn: xous::CID, trng_conn: xous::CID, canvases: &mut Fnv }) } -fn tv_draw(gfx_conn: xous::CID, trng_conn: xous::CID, canvases: &mut FnvIndexMap, tv: &mut TextView) -> Result<(), xous::Error> { - /* - 1. figure out text bounds - 2. clear background, if requested - 3. draw surrounding rectangle, if requested - 4. draw text - */ - - /* - info!("GAM: tv_draw canvas list"); - for (&k, &v) in canvases.iter() { - info!("gid: {:?}, canvas: {:?}", k, v); - }*/ - - if let Some(canvas) = canvases.get_mut(&tv.get_canvas_gid()) { - // first, figure out if we should even be drawing to this canvas. - if canvas.is_drawable() { - // figure out text bounds: figure out how wide our text is, to start with. - graphics_server::set_string_clipping(gfx_conn, canvas.clip_rect().into()).expect("GAM: can't set clipping"); - - } else { - info!("GAM: attempt to draw TextView on non-drawable canvas. Not fatal, but request ignored."); - return Ok(()) - } - - - } else { - info!("GAM: bogus GID in TextView, not doing anything in response to draw request."); - return Ok(()) // silently fail if a bogus Gid is given??? - } - - Ok(()) - -} - #[xous::xous_main] fn xmain() -> ! { log_server::init_wait().unwrap(); @@ -203,16 +168,35 @@ fn xmain() -> ! { archived_value_mut::(Pin::new(buf.as_mut()), m.id as usize) }; use rkyv::Write; - //let mut writer: rkyv::ArchiveBuffer = rkyv::ArchiveBuffer::new(buf); let mut writer = rkyv::ArchiveBuffer::new(xous::XousBuffer::new(4096)); match &*value { rkyv::Archived::::RenderTextView(rtv) => { let mut tv = rtv.unarchive(); match tv.get_op() { TextOp::Nop => (), - TextOp::Render => { + TextOp::Render | TextOp::ComputeBounds => { info!("GAM: render request for {:?}", tv); - tv_draw(gfx_conn, trng_conn, &mut canvases, &mut tv).expect("GAM: can't render TextView"); + if tv.get_op() == TextOp::ComputeBounds { + tv.dry_run = true; + } else { + tv.dry_run = false; + } + + if let Some(canvas) = canvases.get_mut(&tv.get_canvas_gid()) { + // first, figure out if we should even be drawing to this canvas. + if canvas.is_drawable() { + // set the clip rectangle according to the canvas' location + tv.clip_rect = Some(canvas.clip_rect().into()); + + // issue the draw command + graphics_server::draw_textview(gfx_conn, &mut tv).expect("GAM: text view draw could not complete."); + } else { + info!("GAM: attempt to draw TextView on non-drawable canvas. Not fatal, but request ignored."); + } + } else { + info!("GAM: bogus GID in TextView, not doing anything in response to draw request."); + // silently fail if a bogus Gid is given??? + } }, TextOp::ComputeBounds => { diff --git a/services/gam/src/status.rs b/services/gam/src/status.rs index 2c40866f3..0fc8c30c6 100644 --- a/services/gam/src/status.rs +++ b/services/gam/src/status.rs @@ -34,7 +34,7 @@ pub fn status_thread(canvas_gid: [u32; 4]) { uptime_tv.untrusted = false; uptime_tv.style = blitstr::GlyphStyle::Small; uptime_tv.draw_border = false; - uptime_tv.x_margin = 0; uptime_tv.y_margin = 0; + uptime_tv.margin = Point::new(0, 0); write!(uptime_tv, "Booting up...").expect("GAM|status: couldn't init uptime text"); info!("GAM|status: screensize as reported: {:?}", screensize); info!("GAM|status: uptime initialized to '{:?}'", uptime_tv); @@ -45,8 +45,7 @@ pub fn status_thread(canvas_gid: [u32; 4]) { Point::new(screensize.x, small_height)))); battstats_tv.style = blitstr::GlyphStyle::Small; battstats_tv.draw_border = false; - battstats_tv.x_margin = 0; battstats_tv.y_margin = 0; - battstats_tv.alignment = TextAlignment::Right; + battstats_tv.margin = Point::new(0, 0); let mut stats: BattStats = BattStats::default(); let mut last_time: u64 = ticktimer_server::elapsed_ms(ticktimer_conn).unwrap(); @@ -82,7 +81,7 @@ pub fn status_thread(canvas_gid: [u32; 4]) { if let Ok(elapsed_time) = ticktimer_server::elapsed_ms(ticktimer_conn) { if elapsed_time - last_time > 500 { - info!("GAM|status: size of TextView type: {} bytes", core::mem::size_of::()); + //info!("GAM|status: size of TextView type: {} bytes", core::mem::size_of::()); // info!("GAM|status: periodic tasks: updating uptime, requesting battstats"); last_time = elapsed_time; com::get_batt_stats_nb(com_conn).expect("Can't get battery stats from COM"); diff --git a/services/graphics-server/src/api.rs b/services/graphics-server/src/api.rs index 7c1e1b233..6202c2602 100644 --- a/services/graphics-server/src/api.rs +++ b/services/graphics-server/src/api.rs @@ -49,7 +49,7 @@ impl hash32::Hash for Gid { } } -#[derive(Debug, rkyv::Archive)] +#[derive(Debug, rkyv::Archive, rkyv::Unarchive)] pub enum Opcode { /// Flush the buffer to the screen Flush, @@ -97,7 +97,7 @@ pub enum Opcode { QueryGlyphProps(GlyphStyle), // draws a textview - // TextView(&'a mut TextView), // redo this as an rkyv object, which has no lifetime requirement + DrawTextView(TextView), } impl core::convert::TryFrom<& Message> for Opcode { diff --git a/services/graphics-server/src/api/shapes.rs b/services/graphics-server/src/api/shapes.rs index dafb65ef9..27c38de6c 100644 --- a/services/graphics-server/src/api/shapes.rs +++ b/services/graphics-server/src/api/shapes.rs @@ -2,7 +2,7 @@ use crate::op::{HEIGHT, WIDTH}; use crate::api::{Point, DrawStyle}; use blitstr_ref as blitstr; use blitstr::{ClipRect}; -use core::cmp::{max, min}; +use core::{cmp::{max, min}, ops::Add}; #[derive(Debug, Clone, Copy, rkyv::Archive, rkyv::Unarchive)] pub struct Rectangle { @@ -71,6 +71,10 @@ impl Rectangle { ((other.br.x >= self.tl.x) && (other.br.x <= self.br.x)) && ((other.br.y >= self.tl.y) && (other.br.y <= self.br.y)) } + pub fn intersects_point(&self, point: Point) -> bool { + ((point.x >= self.tl.x) && (point.x <= self.br.x)) && + ((point.y >= self.tl.y) && (point.y <= self.br.y)) + } pub fn new_coords_with_style( x0: i16, y0: i16, @@ -104,6 +108,12 @@ impl Rectangle { pub fn y1(&self) -> u32 { self.br.y as u32 } + pub fn translate(&mut self, offset: Point) { + self.tl.x += offset.x; + self.br.x += offset.x; + self.tl.y += offset.y; + self.br.y += offset.y; + } /// Make a rectangle of the full screen size pub fn full_screen() -> Rectangle { @@ -186,4 +196,27 @@ impl Circle { style, } } -} \ No newline at end of file +} + +//////////////////////// Rounded Rectangle +#[derive(Debug, Clone, Copy, rkyv::Archive, rkyv::Unarchive)] +pub struct RoundedRectangle { + pub border: Rectangle, // drawstyle is inherited from the Rectangle + pub radius: i16, +} +impl RoundedRectangle { + pub fn new(rr: Rectangle, r: i16) -> RoundedRectangle { + let mut r_adj = r; + // disallow radii that are greater than 2x the width of the rectangle + if r > (rr.br.x - rr.tl.x) { + r_adj = 0; + } + if r > (rr.br.y - rr.tl.y) { + r_adj = 0; + } + RoundedRectangle { + border: rr, + radius: r_adj, + } + } +} diff --git a/services/graphics-server/src/api/text.rs b/services/graphics-server/src/api/text.rs index 288ed641d..ad5481865 100644 --- a/services/graphics-server/src/api/text.rs +++ b/services/graphics-server/src/api/text.rs @@ -4,13 +4,7 @@ use blitstr::{GlyphStyle, Cursor}; use log::info; -#[derive(Debug, Copy, Clone, rkyv::Archive, rkyv::Unarchive)] -pub enum TextAlignment { - Left, - Center, - Right, -} - +/// coordinates are local to the canvas, not absolute to the screen #[derive(Debug, Copy, Clone, rkyv::Archive, rkyv::Unarchive)] pub enum TextBounds { // fixed width and height in a rectangle @@ -23,12 +17,12 @@ pub enum TextBounds { GrowableFromBl(Point, u16), } -#[derive(Debug, Copy, Clone, rkyv::Archive, rkyv::Unarchive)] +#[derive(Debug, Copy, Clone, rkyv::Archive, rkyv::Unarchive, PartialEq)] // operations that may be requested of a TextView when sent to GAM pub enum TextOp { Nop, Render, - ComputeBounds, + ComputeBounds, // maybe we don't need this } impl Into for TextOp { fn into(self) -> usize { @@ -72,26 +66,27 @@ pub struct TextView { pub draw_order: u32, // offsets for text drawing -- exactly one of the following options should be specified + // note that the TextBounds coordinate system is local to the canvas, not the screen pub bounds_hint: TextBounds, - bounds_computed: Option, // is Some(Rectangle) if bounds have been computed and text has not been modified + pub bounds_computed: Option, // is Some(Rectangle) if bounds have been computed and text has not been modified + pub overflow: Option, // indicates if the text has overflowed the canvas, set by the drawing routine + pub dry_run: bool, // set to true if no drawing is desired and we just want to compute the bounds pub style: GlyphStyle, - text: xous::String::<3072>, - length: u32, - pub alignment: TextAlignment, + pub text: xous::String::<3072>, pub cursor: Cursor, pub draw_border: bool, - pub clear_area: bool, + pub clear_area: bool, // you almost always want this to be true pub border_width: u16, - pub rounded_border: bool, - pub x_margin: u16, - pub y_margin: u16, + pub rounded_border: Option, // radius of the rounded border, if applicable + pub margin: Point, // this field specifies the beginning and end of a "selected" region of text pub selected: Option<[u32; 2]>, canvas: Gid, // GID of the canvas to draw on + pub clip_rect: Option, // this is set by the GAM to the canvas' clip_rect; needed by gfx for drawing. Note this is in screen coordinates. } impl TextView { pub fn new(canvas: Gid, draw_order: u32, bounds_hint: TextBounds) -> Self { @@ -101,41 +96,26 @@ impl TextView { token: None, invert: false, draw_order, + clip_rect: None, bounds_hint, bounds_computed: None, style: GlyphStyle::Regular, text: xous::String::<3072>::new(), - length: 0, - alignment: TextAlignment::Left, cursor: Cursor::new(0,0,0), draw_border: true, border_width: 1, - rounded_border: false, - x_margin: 4, - y_margin: 4, + rounded_border: None, + margin: Point { x: 4, y: 4 }, selected: None, canvas, clear_area: true, + overflow: None, + dry_run: false, } } pub fn set_op(&mut self, op: TextOp) { self.operation = op; } pub fn get_op(&self) -> TextOp { self.operation } pub fn get_canvas_gid(&self) -> Gid { self.canvas } - pub fn get_bounds_computed(&self) -> Option { self.bounds_computed } - pub fn compute_bounds(&mut self) -> Result<(), xous::Error> { - match self.bounds_hint { - TextBounds::BoundingBox(r) => { - info!("GFX/text - r {:?}", r); - self.bounds_computed = Some(r); - info!("GFX/text - bounds_computed {:?}", self.bounds_computed); - }, - _=> todo!("other dynamic bounds computations not yet implemented"), - } - Ok(()) - } - pub fn set_computed_bounds(&mut self, r: Option) { - self.bounds_computed = r; - } pub fn to_str(&self) -> &str { self.text.as_str().unwrap() @@ -153,17 +133,17 @@ impl TextView { self.bounds_computed = t.bounds_computed; self.style = t.style; self.text = t.text; - self.length = t.length; - self.alignment = t.alignment; self.cursor = t.cursor; self.draw_border = t.draw_border; self.clear_area = t.clear_area; self.border_width = t.border_width; self.rounded_border = t.rounded_border; - self.x_margin = t.x_margin; - self.y_margin = t.y_margin; + self.margin = t.margin; self.selected = t.selected; self.canvas = t.canvas; + self.overflow = t.overflow; + self.clip_rect = t.clip_rect; + self.dry_run = t.dry_run; } } diff --git a/services/graphics-server/src/lib.rs b/services/graphics-server/src/lib.rs index 6402a098d..c5407a6f5 100644 --- a/services/graphics-server/src/lib.rs +++ b/services/graphics-server/src/lib.rs @@ -2,7 +2,7 @@ // pub mod size; pub mod api; -pub use api::{Circle, DrawStyle, Line, PixelColor, Point, Rectangle, TextView, TextBounds, Gid, TextOp, TextAlignment}; +pub use api::{Circle, DrawStyle, Line, PixelColor, Point, Rectangle, TextView, TextBounds, Gid, TextOp}; use blitstr_ref as blitstr; pub use blitstr::{ClipRect, Cursor, GlyphStyle}; use xous::String; @@ -109,10 +109,28 @@ pub fn glyph_height_hint(cid: CID, glyph: GlyphStyle) -> Result Result<(), xous::Error> { - /*/ - let mut sendable_tv = Sendable::new(tv) - .expect("can't create sendable TextView structure"); - sendable_tv.lend_mut(cid, 0x100).map(|_| ()) */ +pub fn draw_textview(cid: CID, tv: &mut TextView) -> Result<(), xous::Error> { + use log::info; + use rkyv::Write; + use rkyv::Unarchive; + + let mut rkyv_tv = api::Opcode::DrawTextView(*tv); + let mut writer = rkyv::ArchiveBuffer::new(xous::XousBuffer::new(4096)); + let pos = writer.archive(&rkyv_tv).expect("couldn't archive textview"); + let mut xous_buffer = writer.into_inner(); + + xous_buffer.lend_mut(cid, pos as u32).expect("draw_textview operation failure"); + + let returned = unsafe { rkyv::archived_value::(xous_buffer.as_ref(), pos)}; + if let rkyv::Archived::::DrawTextView(result) = returned { + let tvr: TextView = result.unarchive(); + tv.bounds_computed = tvr.bounds_computed; + tv.cursor = tvr.cursor; + } else { + let tvr = returned.unarchive(); + info!("draw_textview saw a return of {:?}", tvr); + panic!("draw_textview got a return value from the server that isn't expected or handled"); + } + Ok(()) } diff --git a/services/graphics-server/src/main.rs b/services/graphics-server/src/main.rs index 426118595..4b3f40338 100644 --- a/services/graphics-server/src/main.rs +++ b/services/graphics-server/src/main.rs @@ -8,7 +8,7 @@ use api::Opcode; use xous::buffer; use core::pin::Pin; -use rkyv::{archived_value, Unarchive}; +use rkyv::{archived_value, Unarchive, archived_value_mut}; use core::convert::TryInto; mod backend; @@ -20,7 +20,7 @@ use core::convert::TryFrom; mod logo; -use api::{DrawStyle, PixelColor, Rectangle}; +use api::{DrawStyle, PixelColor, Rectangle, TextBounds, RoundedRectangle}; use blitstr_ref as blitstr; mod fontmap; @@ -76,6 +76,23 @@ fn xmain() -> ! { let sid = xous_names::register_name(xous::names::SERVER_NAME_GFX).expect("GFX: can't register server"); info!("GFX: Server listening on address {:?}", sid); + if false { + // leave this test case around + // for some reason, the top right quadrant draws an extra pixel inside the fill area + // when a fill color of "Light" is specified. However, if `None` fill is specified, it + // works correctly. This is really puzzling, because the test for filled drawing happens + // after the test for border drawing. + use api::Point; + let mut r = Rectangle::new(Point::new(20, 200), Point::new(151, 301)); + r.style = DrawStyle { + fill_color: Some(PixelColor::Light), + stroke_color: Some(PixelColor::Dark), + stroke_width: 1, + }; + let rr = RoundedRectangle::new(r, 16); + op::rounded_rectangle(display.native_buffer(), rr); + } + display.redraw(); loop { let msg = xous::receive_message(sid).unwrap(); @@ -113,20 +130,108 @@ fn xmain() -> ! { blitstr::xor_char ); }, - rkyv::Archived::::SimulateString(rkyv_s) => { - let s: xous::String<4096> = rkyv_s.unarchive(); - blitstr::paint_str( - display.native_buffer(), - current_string_clip.into(), - &mut current_cursor, - current_glyph.into(), - s.as_str().unwrap(), - false, - blitstr::simulate_char - ); - }, _ => panic!("GFX: invalid response from server -- corruption occurred in MemoryMessage") }; + } else if let xous::Message::MutableBorrow(m) = &msg.body { + let mut buf = unsafe { xous::XousBuffer::from_memory_message(m) }; + let value = unsafe { + archived_value_mut::(Pin::new(buf.as_mut()), m.id as usize) + }; + use rkyv::Write; + let mut writer = rkyv::ArchiveBuffer::new(xous::XousBuffer::new(4096)); + let debugtv: bool = true; + match &*value { + rkyv::Archived::::DrawTextView(rtv) => { + let mut tv = rtv.unarchive(); + + let paintfn = if tv.dry_run { + if debugtv { info!("GFX(TV): doing dry run"); } + blitstr::simulate_char + } else { + if debugtv { info!("GFX(TV): drawing"); } + blitstr::xor_char + }; + + /* + 1. figure out text bounds + 2. clear background, if requested + 3. draw surrounding rectangle, if requested + 4. draw text + */ + // first compute the bounding box, if it isn't computed + if tv.bounds_computed.is_none() { + match tv.bounds_hint { + TextBounds::BoundingBox(r) => { + tv.bounds_computed = Some(r); + }, + TextBounds::GrowableFromBr(br, width) => { + unimplemented!() + }, + TextBounds::GrowableFromTl(tl, width) => { + unimplemented!() + }, + TextBounds::GrowableFromBl(bl, width) => { + unimplemented!() + } + } + } + if debugtv { info!("GFX(TV): computed bounds {:?}", tv.bounds_computed); } + + // clear the bounding box if requested + let mut clear_rect = tv.bounds_computed.unwrap(); + clear_rect.translate(tv.clip_rect.unwrap().tl); + let bordercolor = if tv.draw_border { + Some(PixelColor::Dark) + } else { + None + }; + let borderwidth: i16 = if tv.draw_border { + 1 + } else { + 0 + }; + let fillcolor = if tv.clear_area { + Some(PixelColor::Light) + } else { + None + }; + + clear_rect.style = DrawStyle { + fill_color: fillcolor, + stroke_color: bordercolor, + stroke_width: borderwidth, + }; + if tv.rounded_border.is_some() { + op::rounded_rectangle(display.native_buffer(), + RoundedRectangle::new(clear_rect, tv.rounded_border.unwrap() as _)); + } else { + if debugtv { info!("GFX(TV): clearing rectangle {:?}", clear_rect); } + op::rectangle(display.native_buffer(), clear_rect); + } + + + // this is the actual draw operation + clear_rect.translate(tv.margin); + + /////// TODO: I think we need to clip the clear_rect.tl.x/y to within the screen area for this to be valid? + let mut ref_cursor = blitstr::Cursor{ + pt: blitstr::Pt{x: clear_rect.tl.x as u32, y: clear_rect.tl.y as u32}, + line_height: 0, + }; + if debugtv { info!("GFX(TV): paint_str with {:?} | {:?} | {:?} | {:?}", clear_rect, ref_cursor, tv.style, tv.text); } + blitstr::paint_str( + display.native_buffer(), + clear_rect.into(), + &mut ref_cursor, + tv.style.into(), + tv.text.as_str().unwrap(), + false, + paintfn + ); + tv.cursor = ref_cursor; + }, + _ => panic!("GFX: invalid mutable borrow message"), + } } else if let Ok(opcode) = Opcode::try_from(&msg.body) { // info!("GFX: Opcode: {:?}", opcode); match opcode { diff --git a/services/graphics-server/src/op.rs b/services/graphics-server/src/op.rs index fe2c8d72e..d18de9e90 100644 --- a/services/graphics-server/src/op.rs +++ b/services/graphics-server/src/op.rs @@ -1,4 +1,4 @@ -use crate::api::{Circle, DrawStyle, Line, Pixel, PixelColor, Point, Rectangle, TextView}; +use crate::api::{Circle, DrawStyle, Line, Pixel, PixelColor, Point, Rectangle, TextView, RoundedRectangle}; use log::info; use blitstr_ref as blitstr; @@ -25,7 +25,7 @@ fn put_pixel(fb: &mut LcdFB, x: i16, y: i16, color: PixelColor) { clip_y = LCD_PX_PER_LINE - 1; } - if color == PixelColor::Dark { + if color == PixelColor::Light { fb[(clip_x + clip_y * LCD_WORDS_PER_LINE * 32) / 32] |= 1 << (clip_x % 32) } else { fb[(clip_x + clip_y * LCD_WORDS_PER_LINE * 32) / 32] &= !(1 << (clip_x % 32)) @@ -233,21 +233,271 @@ pub fn rectangle(fb: &mut LcdFB, rect: Rectangle) { } } +/////////////////////////////////////////////////// rounded rectangle -pub fn textview(fb: &mut LcdFB, tv: &mut TextView) { - tv.compute_bounds().expect("GFX: couldn't compute bounds of textview"); - - let mut c = tv.cursor.clone(); - // at this point, bounds_computed *must* be Some() - info!("GFX: painting tv with cursor {:?}, clip {:?}, text {}, style {:?}", c, tv.get_bounds_computed().unwrap(), tv, tv.style); - blitstr::paint_str( - fb, - tv.get_bounds_computed().unwrap().into(), - &mut c, - tv.style, - tv.to_str(), - false, - blitstr::xor_char - ); - tv.cursor = c; +#[derive(Debug, Clone, Copy)] +pub enum Quadrant { + TopLeft, + TopRight, + BottomLeft, + BottomRight, } + +#[derive(Debug, Copy, Clone)] +pub struct QuadrantIterator { + center: Point, + radius: u16, + style: DrawStyle, + p: Point, + quad: Quadrant, +} + +impl Iterator for QuadrantIterator { + type Item = Pixel; + + // https://stackoverflow.com/questions/1201200/fast-algorithm-for-drawing-filled-circles + fn next(&mut self) -> Option { + // If border or stroke colour is `None`, treat entire object as transparent and exit early + if self.style.stroke_color.is_none() && self.style.fill_color.is_none() { + return None; + } + + let inner_radius = self.radius as i16 - self.style.stroke_width + 1; + let outer_radius = self.radius as i16; + + let inner_radius_sq = inner_radius * inner_radius; + let outer_radius_sq = outer_radius * outer_radius; + + loop { + let t = self.p; + let len = t.x * t.x + t.y * t.y; + + let is_border = len > (inner_radius_sq - inner_radius) && len < (outer_radius_sq + inner_radius); + + let is_fill = len <= outer_radius_sq + 1; + + let item = if is_border && self.style.stroke_color.is_some() { + Some(Pixel( + self.center + t, + self.style.stroke_color.expect("Border color not defined"), + )) + } else if is_fill && self.style.fill_color.is_some() { + Some(Pixel( + self.center + t, + self.style.fill_color.expect("Fill color not defined"), + )) + } else { + None + }; + + self.p.x += 1; + + match self.quad { + Quadrant::TopLeft => { + if self.p.x > 0 as i16 { + self.p.x = -(self.radius as i16); + self.p.y += 1; + } + if self.p.y > 0 as i16 { + break item; + } + if item.is_some() { + break item; + } + }, + Quadrant::TopRight => { + if self.p.x > self.radius as i16 { + self.p.x = 0; + self.p.y += 1; + } + if self.p.y > 0 as i16 { + break item; + } + if item.is_some() { + break item; + } + }, + Quadrant::BottomLeft => { + if self.p.x > 0 as i16 { + self.p.x = -(self.radius as i16); + self.p.y += 1; + } + if self.p.y > self.radius as i16 { + break item; + } + if item.is_some() { + break item; + } + }, + Quadrant::BottomRight => { + if self.p.x > self.radius as i16 { + self.p.x = 0; + self.p.y += 1; + } + if self.p.y > self.radius as i16 { + break item; + } + if item.is_some() { + break item; + } + } + } + } + } +} + +pub fn quadrant(fb: &mut LcdFB, circle: Circle, quad: Quadrant) { + let starting_pixel = match quad { + Quadrant::TopLeft => { + Point::new( -(circle.radius as i16), -(circle.radius as i16)) + }, + Quadrant::TopRight => { + Point::new( 0, -(circle.radius as i16)) + }, + Quadrant::BottomLeft => { + Point::new( -(circle.radius as i16), 0) + }, + Quadrant::BottomRight => { + Point::new( 0, 0 ) + } + }; + let q = QuadrantIterator { + center: circle.center, + radius: circle.radius as _, + style: circle.style, + p: starting_pixel, + quad: quad, + }; + + for pixel in q { + put_pixel(fb, pixel.0.x, pixel.0.y, pixel.1); + } +} + + +/// Pixel iterator for each pixel in the rect border +/// lifted from embedded-graphics crate +#[derive(Debug, Clone, Copy)] +pub struct RoundedRectangleIterator { + top_left: Point, + bottom_right: Point, + style: DrawStyle, + radius: i16, + p: Point, + // the four quadrants for drawing the rounded corners + tlq: Rectangle, + trq: Rectangle, + blq: Rectangle, + brq: Rectangle, +} + +impl Iterator for RoundedRectangleIterator { + type Item = Pixel; + + fn next(&mut self) -> Option { + // Don't render anything if the rectangle has no border or fill color. + if self.style.stroke_color.is_none() && self.style.fill_color.is_none() { + return None; + } + loop { + let mut out = None; + + // Finished, i.e. we're below the rect + if self.p.y > self.bottom_right.y { + break None; + } + + // suppress the output pixel if we happen to be in the corner quadrant area + if self.tlq.intersects_point(self.p) || self.trq.intersects_point(self.p) || + self.blq.intersects_point(self.p) || self.brq.intersects_point(self.p) { + out = None + } else { + let border_width = self.style.stroke_width; + let tl = self.top_left; + let br = self.bottom_right; + + // Border + if ( + // Top border + (self.p.y >= tl.y && self.p.y < tl.y + border_width) + // Bottom border + || (self.p.y <= br.y && self.p.y > br.y - border_width) + // Left border + || (self.p.x >= tl.x && self.p.x < tl.x + border_width) + // Right border + || (self.p.x <= br.x && self.p.x > br.x - border_width) + ) && self.style.stroke_color.is_some() + { + out = Some(Pixel( + self.p, + self.style.stroke_color.expect("Expected stroke"), + )); + } + // Fill + else if let Some(fill) = self.style.fill_color { + out = Some(Pixel(self.p, fill)); + } + } + + self.p.x += 1; + + // Reached end of row? Jump down one line + if self.p.x > self.bottom_right.x { + self.p.x = self.top_left.x; + self.p.y += 1; + } + + + if out.is_some() { + break out; + } + } + } +} + +pub fn rounded_rectangle(fb: &mut LcdFB, rr: RoundedRectangle) { + /// left off: compute the four quadrants + // call the rr iterator on the rectangle + // then call it on one each of the four circle quadrants + + let rri = RoundedRectangleIterator { + top_left: rr.border.tl, + bottom_right: rr.border.br, + style: rr.border.style, + radius: rr.radius, + p: rr.border.tl, + tlq: Rectangle::new( + rr.border.tl, + Point::new(rr.border.tl.x + rr.radius, rr.border.tl.y + rr.radius)), + trq: Rectangle::new( + Point::new(rr.border.br.x - rr.radius, rr.border.tl.y), + Point::new(rr.border.br.x, rr.border.tl.y + rr.radius)), + blq: Rectangle::new( + Point::new(rr.border.tl.x, rr.border.br.y - rr.radius), + Point::new(rr.border.tl.x + rr.radius, rr.border.br.y)), + brq: Rectangle::new( + Point::new(rr.border.br.x - rr.radius, rr.border.br.y - rr.radius), + rr.border.br), + }; + // draw the body + for pixel in rri { + put_pixel(fb, pixel.0.x, pixel.0.y, pixel.1); + } + // now draw the corners + quadrant(fb, + Circle::new_with_style(rri.tlq.br, rr.radius, rr.border.style), + Quadrant::TopLeft + ); + quadrant(fb, + Circle::new_with_style(Point::new(rri.trq.tl.x, rri.trq.br.y), rr.radius, rr.border.style), + Quadrant::TopRight + ); + quadrant(fb, + Circle::new_with_style(Point::new(rri.blq.br.x, rri.blq.tl.y), rr.radius, rr.border.style), + Quadrant::BottomLeft + ); + quadrant(fb, + Circle::new_with_style(rri.brq.tl, rr.radius, rr.border.style), + Quadrant::BottomRight + ); +} \ No newline at end of file diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c2e2474ea..ff6839bf9 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -186,8 +186,8 @@ fn renode_image(debug: bool, packages: &[&str]) -> Result<(), DynError> { fn run(debug: bool) -> Result<(), DynError> { let stream = if debug { "debug" } else { "release" }; let init = [ - "shell", - //"gam", + //"shell", + "gam", //"benchmark", //"benchmark-target", "log-server",