From dc62753629b9a1cff51b63754f1eef60dbc32169 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 31 Oct 2021 15:48:01 +0100 Subject: [PATCH] Separate theming engine from widget painter --- src/ui/mod.rs | 1 + src/ui/painter.rs | 126 +++++++++++++--------------------------------- src/ui/theme.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 90 deletions(-) create mode 100644 src/ui/theme.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f9f3ae4..ba82a3f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,3 +3,4 @@ pub use ui::draw; pub use ui::redraw_quick; mod painter; +mod theme; diff --git a/src/ui/painter.rs b/src/ui/painter.rs index 055cf31..4620947 100644 --- a/src/ui/painter.rs +++ b/src/ui/painter.rs @@ -1,49 +1,6 @@ -type Color = (f64, f64, f64, f64); +use log::debug; -struct Theme { - background: Color, - - frame: Color, - frame_width: f64, - - input_bg: Color, - input_font_color: Color, - input_font_size: f64, - - result_bg: Color, - result_bg_selected: Color, - result_font_color: Color, - result_font_selected: Color, - result_font_size: f64, - - divider: Color, - divider_width: f64 -} - -impl Default for Theme { - fn default() -> Self { - let bg_color = (0.5, 0.5, 1.0, 1.0); - Theme { - background: bg_color, - - frame: (1.0, 0.0, 0.0, 1.0), - frame_width: 5.0, - - input_bg: (0.5, 0.5, 1.0, 1.0), - input_font_color: (0.0, 0.0, 1.0, 1.0), - input_font_size: 20.0, - - result_bg: (0.5, 0.5, 1.0, 1.0), - result_bg_selected: (0.5, 0.5, 0.9, 1.0), - result_font_color: (0.0, 0.0, 1.0, 1.0), - result_font_selected: (0.5, 0.5, 0.5, 1.0), - result_font_size: 20.0, - - divider: (0.0, 0.0, 0.0, 1.0), - divider_width: 1.0 - } - } -} +use super::theme::{Theme, ThemedContextExt}; pub struct Painter<'a> { context: &'a cairo::Context, @@ -59,25 +16,39 @@ impl<'a> Painter<'a> { { let ctx = self.context; - // reset clip so that we can draw the entire background + // reset clip so that we can draw the entire background of the surface ctx.reset_clip(); - // fill the background - let (r,g,b,a) = self.theme.background; - ctx.set_source_rgba(r, g, b, a); + // paint base color to clip region (i.e. entire surface) + ctx.theme_color_base(&self.theme); ctx.set_operator(cairo::Operator::Source); - ctx.paint().unwrap(); + ctx.paint().expect("Could not paint background"); // draw the border let (x1,y1,x2,y2) = ctx.clip_extents().unwrap(); - let (r,g,b,a) = self.theme.frame; - ctx.rectangle(x1, y1, x2-x1, y2-y1); - ctx.set_source_rgba(r, g, b, a); - let frame_width = self.device_to_user(self.theme.frame_width); - ctx.set_line_width(frame_width); + debug!{"Border width: {}", self.theme.border(ctx)}; + let border = self.theme.border(ctx)/2.0; + let (x1,y1,x2,y2) = (x1+border,y1+border,x2-x1-border,y2-y1-border); + debug!{"Border rect: {},{},{},{}", x1, y1, x2, y2}; + ctx.rectangle(x1,y1,x2-x1,y2-y1); + ctx.theme_border(&self.theme); ctx.stroke().unwrap(); - ctx.rectangle(x1+(frame_width/2.0), y1+(frame_width/2.0), x2-x1-frame_width, y2-y1-frame_width); + // ctx.reset_clip(); + // // finally, clip any future content so the border stays alive + let (x1,y1,x2,y2) = ctx.clip_extents().unwrap(); // the path of the clip + // // region, i.e. the surface + + debug!{"Border extent: {},{},{},{}", x1, y1, x2, y2}; + let border = self.theme.border(ctx); // width of the border, note + // that half of the border is outside + // the clip_extents + +// let (x1,y1,x2,y2) = (x1+border, y1+border, x2-border, y2-border); + debug!{"Border rect: {},{},{},{}", x1, y1, x2, y2}; + let (x1,y1,x2,y2) = (x1+border,y1+border,x2-x1-border,y2-y1-border-border); + debug!{"Border clip: {},{},{},{}", x1, y1, x2, y2}; + ctx.rectangle(x1,y1,x2,y2); ctx.clip(); } @@ -88,19 +59,13 @@ impl<'a> Painter<'a> { let (x, y): (f64, f64) = (x.into(), y.into()); // draw background box + ctx.theme_color_base(&self.theme); ctx.rectangle(x, y, self.clip_width(), 1.0); - let (r,g,b,a) = self.theme.input_bg; - ctx.set_source_rgba(r,g,b,a); ctx.fill().unwrap(); // draw text - ctx.select_font_face ("serif", cairo::FontSlant::Normal, cairo::FontWeight::Normal); - let font_size = self.device_to_user(self.theme.input_font_size); - ctx.set_font_size(font_size); - let (r,g,b,a) = self.theme.input_font_color; - ctx.set_source_rgba(r,g,b,a); ctx.move_to(x + 0.2, y + 0.8); - ctx.show_text(input).unwrap(); + ctx.theme_text(&input, false, &[], &self.theme); } pub fn result_box(&self, x: T, y: T, result: &'a str, indices: &[usize], selected: bool) @@ -111,28 +76,14 @@ impl<'a> Painter<'a> { // draw background box ctx.rectangle(x.into(), y.into(), self.clip_width(), 1.0); - let (r,g,b,a) = if selected { self.theme.result_bg_selected } - else { self.theme.result_bg }; - ctx.set_source_rgba(r,g,b,a); + if selected { ctx.theme_color_highlight(&self.theme) } + else { ctx.theme_color_base(&self.theme) }; + ctx.set_operator(cairo::Operator::Source); ctx.fill().unwrap(); // draw text - let font_size = self.device_to_user(self.theme.result_font_size); - ctx.set_font_size(font_size); - let (r,g,b,a) = if selected { self.theme.result_font_selected } - else { self.theme.result_font_color }; - ctx.set_source_rgba(r,g,b,a); ctx.move_to (x+0.2, y+0.8); - - for i in 0..result.len() { - if indices.contains(&i) { - ctx.select_font_face ("serif", cairo::FontSlant::Normal, cairo::FontWeight::Bold); - ctx.show_text(&result[i..i+1]).unwrap(); - } else { - ctx.select_font_face ("serif", cairo::FontSlant::Normal, cairo::FontWeight::Normal); - ctx.show_text(&result[i..i+1]).unwrap(); - } - } + ctx.theme_text(&result, selected, indices, &self.theme) } pub fn divider(&self, x: T, y: T) @@ -141,18 +92,13 @@ impl<'a> Painter<'a> { let ctx = self.context; let (x, y): (f64, f64) = (x.into(), y.into()); + ctx.theme_divider(&self.theme); ctx.move_to(x, y); - - let dash_width = self.device_to_user(self.theme.divider_width); - ctx.set_line_width(dash_width); ctx.line_to(self.clip_width(), y); - ctx.set_dash(&[self.device_to_user(3.0)], 0.0); - let (r,g,b,a) = self.theme.divider; - ctx.set_source_rgba(r,g,b,a); ctx.stroke().unwrap(); - ctx.rectangle(x, y+dash_width, self.clip_width(), self.clip_height()-dash_width); - ctx.clip(); +// ctx.rectangle(x, y+dash_width, self.clip_width(), self.clip_height()-dash_width); +// ctx.clip(); } fn device_to_user(&self, point: f64) -> f64 { diff --git a/src/ui/theme.rs b/src/ui/theme.rs new file mode 100644 index 0000000..344d4c0 --- /dev/null +++ b/src/ui/theme.rs @@ -0,0 +1,125 @@ +// (r,g,b,a) +#[derive(Clone, Copy)] +pub struct Color(f64, f64, f64, f64); +// (name, size) +pub struct Font(String, f64); + +struct ColorScheme { + base: Color, + border: Color, + highlight: Color, + + divider: Color, + + text: Color, + text_highlight: Color +} + +pub struct Theme { + colors: ColorScheme, + font: Font, + + border: f64, + divider: f64 +} + +impl Theme { + pub fn border(&self, ctx: &cairo::Context) -> f64 { + ctx.device_to_user_point(self.border) + } +} + +impl Default for Theme { + fn default() -> Self { + let colors = ColorScheme { + base: Color(0.13, 0.05, 0.23, 0.9), + border: Color(0.23, 0.05, 0.11, 1.0), + highlight: Color(0.12, 0.04, 0.08, 0.9), + + divider: Color(0.23, 0.05, 0.11, 1.0), + + text: Color(0.87, 0.95, 0.77, 1.0), + text_highlight: Color(0.6, 0.8, 0.4, 1.0) + }; + + Theme { + colors, + font: Font("sans".into(), 17.0), + + border: 2.0, + divider: 3.0 + } + } +} + +pub trait ThemedContextExt { + fn theme_color_base(&self, theme: &Theme); + fn theme_color_highlight(&self, theme: &Theme); + + fn theme_border(&self, theme: &Theme); + fn theme_divider(&self, theme: &Theme); + + fn theme_text(&self, text: &str, selected: bool, indices: &[usize], theme: &Theme); + + + fn device_to_user_point(&self, point: f64) -> f64; +} + +impl ThemedContextExt for cairo::Context { + fn theme_color_base(&self, theme: &Theme) { + let Color(r,g,b,a) = theme.colors.base; + self.set_source_rgba(r,g,b,a); + } + + fn theme_color_highlight(&self, theme: &Theme) { + let Color(r,g,b,a) = theme.colors.highlight; + self.set_source_rgba(r,g,b,a); + } + + fn theme_border(&self, theme: &Theme) { + let Color(r,g,b,a) = theme.colors.border; + self.set_source_rgba(r,g,b,a); + let border_width = self.device_to_user_point(theme.border); + self.set_line_width(border_width); + } + + fn theme_divider(&self, theme: &Theme) { + let divider_width = self.device_to_user_point(theme.divider); + self.set_line_width(divider_width); + + self.set_dash(&[self.device_to_user_point(3.0)], 0.0); + + let Color(r,g,b,a) = theme.colors.divider; + self.set_source_rgba(r, g, b, a); + } + + fn theme_text(&self, text: &str, selected: bool, indices: &[usize], theme: &Theme) { + let Color(r,g,b,a) = if selected { theme.colors.text_highlight } + else { theme.colors.text }; + self.set_source_rgba(r,g,b,a); + + let font_size = self.device_to_user_point(theme.font.1); + self.set_font_size(font_size); + + for i in 0..text.len() { + if indices.contains(&i) { + self.select_font_face(&theme.font.0, + cairo::FontSlant::Italic, + cairo::FontWeight::Normal); + } else { + self.select_font_face(&theme.font.0, + cairo::FontSlant::Normal, + cairo::FontWeight::Normal); + } + + self.show_text(&text[i..i+1]) + .expect("Could not draw themed text"); + } + } + + fn device_to_user_point(&self, point: f64) -> f64 { + self.device_to_user(point, point) + .map(|(x,y)| if x>=y {x} else {y}) + .expect("Could not convert device to user space") + } +}