use cairo::{ Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern, TextExtents, }; pub struct AsymLineCutout { pub orientation: gtk::Orientation, pub start_x: f64, pub start_y: f64, pub start_length: f64, pub cutout_length: f64, pub end_length: f64, pub height: f64, pub invert: bool, } impl AsymLineCutout { pub fn draw(&self, pen: &impl Pen) { let dodge = if self.invert { self.height } else { -self.height }; match self.orientation { gtk::Orientation::Horizontal => { pen.move_to(self.start_x, self.start_y); pen.line_to(self.start_x + self.start_length, self.start_y); pen.line_to( self.start_x + self.start_length + self.height, self.start_y + dodge, ); pen.line_to( self.start_x + self.start_length + self.height + self.cutout_length, self.start_y + dodge, ); pen.line_to( self.start_x + self.start_length + self.height + self.cutout_length + (self.height / 2.), self.start_y + dodge / 2., ); pen.line_to( self.start_x + self.start_length + self.height + self.cutout_length + (self.height / 2.) + self.end_length, self.start_y + dodge / 2., ); } gtk::Orientation::Vertical => { pen.move_to(self.start_x, self.start_y); pen.line_to(self.start_x, self.start_y + self.start_length); pen.line_to( self.start_x + dodge, self.start_y + self.start_length + self.height, ); pen.line_to( self.start_x + dodge, self.start_y + self.start_length + self.height + self.cutout_length, ); pen.line_to( self.start_x + dodge / 2., self.start_y + self.start_length + self.height + self.cutout_length + (self.height / 2.), ); pen.line_to( self.start_x + dodge / 2., self.start_y + self.start_length + self.height + self.cutout_length + (self.height / 2.) + self.end_length, ); } _ => panic!("unknown orientation"), } } } // Represents an asymetrical line that starts at one location, then a 45-degree angle and then // another line afterwards. pub struct AsymLine { // Will this be drawn left-to-right or up-to-down? pub orientation: gtk::Orientation, // Starting address pub start_x: f64, pub start_y: f64, // Length of the first segment pub start_length: f64, // Height to dodge over to the next section pub height: f64, // Total length of the entire line. pub end_length: f64, // When normal, the angle dodge is upwards. When inverted, the angle dodge is downwards. pub invert: bool, } impl AsymLine { pub fn draw(&self, pen: &impl Pen) { let dodge = if self.invert { self.height } else { -self.height }; match self.orientation { gtk::Orientation::Horizontal => { pen.move_to(self.start_x, self.start_y); pen.line_to(self.start_x + self.start_length, self.start_y); pen.line_to( self.start_x + self.start_length + self.height, self.start_y + dodge, ); pen.line_to( self.start_x + self.start_length + self.height + self.end_length, self.start_y + dodge, ); } gtk::Orientation::Vertical => {} _ => panic!("unknown orientation"), } } } struct SlashMeter { orientation: gtk::Orientation, start_x: f64, start_y: f64, count: u8, fill_count: u8, height: f64, length: f64, } impl SlashMeter { fn draw(&self, context: &Context) { match self.orientation { gtk::Orientation::Horizontal => { let angle: f64 = 0.8; let run = self.height / angle.tan(); let width = self.length / (self.count as f64 * 2.); for c in 0..self.count { context.set_line_width(1.); let start_x = self.start_x + c as f64 * width * 2.; context.move_to(start_x, self.start_y); context.line_to(start_x + run, self.start_y - self.height); context.line_to(start_x + run + width, self.start_y - self.height); context.line_to(start_x + width, self.start_y); context.line_to(start_x, self.start_y); if c < self.fill_count { let _ = context.fill(); } else { let _ = context.stroke(); } } } gtk::Orientation::Vertical => {} _ => panic!("unknown orientation"), } } } /// Represents a pen for drawing a pattern. This is good for complex patterns that may require /// multiple identical steps. pub trait Pen { /// Move the pen to a location. fn move_to(&self, x: f64, y: f64); /// Draw a line from the current location to the specified destination. fn line_to(&self, x: f64, y: f64); /// Instantiate the line. fn stroke(&self); /// Convert all of the drawing into a pattern that can be painted to a drawing context. fn finish(self) -> Pattern; } pub struct GlowPen { blur_context: Context, draw_context: Context, } impl GlowPen { pub fn new( width: i32, height: i32, line_width: f64, blur_line_width: f64, color: (f64, f64, f64), ) -> Self { let blur_context = Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap(); blur_context.set_line_width(blur_line_width); blur_context.set_source_rgba(color.0, color.1, color.2, 0.5); blur_context.push_group(); blur_context.set_line_cap(LineCap::Round); let draw_context = Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap(); draw_context.set_line_width(line_width); draw_context.set_source_rgb(color.0, color.1, color.2); draw_context.push_group(); draw_context.set_line_cap(LineCap::Round); Self { blur_context, draw_context, } } } impl Pen for GlowPen { fn move_to(&self, x: f64, y: f64) { self.blur_context.move_to(x, y); self.draw_context.move_to(x, y); } fn line_to(&self, x: f64, y: f64) { self.blur_context.line_to(x, y); self.draw_context.line_to(x, y); } fn stroke(&self) { self.blur_context.stroke().expect("to draw the blur line"); self.draw_context .stroke() .expect("to draw the regular line"); } fn finish(self) -> Pattern { let foreground = self.draw_context.pop_group().unwrap(); self.blur_context.set_source(foreground).unwrap(); self.blur_context.paint().unwrap(); self.blur_context.pop_group().unwrap() } } pub struct Text<'a> { content: Vec, context: &'a Context, } impl<'a> Text<'a> { pub fn new(content: String, context: &'a Context, size: f64, width: f64) -> Self { context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold); context.set_font_size(size); let lines = word_wrap(content, context, width); Self { content: lines, context } } pub fn extents(&self) -> TextExtents { self.context.text_extents(&self.content[0]).unwrap() } pub fn draw(&self) { let mut baseline = 0.; for line in self.content.iter() { baseline += self.context.text_extents(line).unwrap().height() + 10.; self.context.move_to(0., baseline); let _ = self.context.show_text(&line); } } } fn word_wrap(content: String, context: &Context, max_width: f64) -> Vec { let mut lines = vec![]; let words: Vec<&str> = content.split_whitespace().collect(); let mut start: usize = 0; let mut line = String::new(); for idx in 0..words.len() + 1 { line = words[start..idx].join(" "); let extents = context.text_extents(&line).unwrap(); if extents.width() > max_width { let line = words[start..idx-1].join(" "); start = idx-1; lines.push(line.clone()); } } if line.len() > 0 { lines.push(line); } lines }