Separate theming engine from widget painter

This commit is contained in:
Armin Friedl 2021-10-31 15:48:01 +01:00
parent 31b355c727
commit dc62753629
3 changed files with 162 additions and 90 deletions

View file

@ -3,3 +3,4 @@ pub use ui::draw;
pub use ui::redraw_quick; pub use ui::redraw_quick;
mod painter; mod painter;
mod theme;

View file

@ -1,49 +1,6 @@
type Color = (f64, f64, f64, f64); use log::debug;
struct Theme { use super::theme::{Theme, ThemedContextExt};
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
}
}
}
pub struct Painter<'a> { pub struct Painter<'a> {
context: &'a cairo::Context, context: &'a cairo::Context,
@ -59,25 +16,39 @@ impl<'a> Painter<'a> {
{ {
let ctx = self.context; 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(); ctx.reset_clip();
// fill the background // paint base color to clip region (i.e. entire surface)
let (r,g,b,a) = self.theme.background; ctx.theme_color_base(&self.theme);
ctx.set_source_rgba(r, g, b, a);
ctx.set_operator(cairo::Operator::Source); ctx.set_operator(cairo::Operator::Source);
ctx.paint().unwrap(); ctx.paint().expect("Could not paint background");
// draw the border // draw the border
let (x1,y1,x2,y2) = ctx.clip_extents().unwrap(); let (x1,y1,x2,y2) = ctx.clip_extents().unwrap();
let (r,g,b,a) = self.theme.frame; 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.rectangle(x1,y1,x2-x1,y2-y1);
ctx.set_source_rgba(r, g, b, a); ctx.theme_border(&self.theme);
let frame_width = self.device_to_user(self.theme.frame_width);
ctx.set_line_width(frame_width);
ctx.stroke().unwrap(); 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(); ctx.clip();
} }
@ -88,19 +59,13 @@ impl<'a> Painter<'a> {
let (x, y): (f64, f64) = (x.into(), y.into()); let (x, y): (f64, f64) = (x.into(), y.into());
// draw background box // draw background box
ctx.theme_color_base(&self.theme);
ctx.rectangle(x, y, self.clip_width(), 1.0); 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(); ctx.fill().unwrap();
// draw text // 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.move_to(x + 0.2, y + 0.8);
ctx.show_text(input).unwrap(); ctx.theme_text(&input, false, &[], &self.theme);
} }
pub fn result_box<T>(&self, x: T, y: T, result: &'a str, indices: &[usize], selected: bool) pub fn result_box<T>(&self, x: T, y: T, result: &'a str, indices: &[usize], selected: bool)
@ -111,28 +76,14 @@ impl<'a> Painter<'a> {
// draw background box // draw background box
ctx.rectangle(x.into(), y.into(), self.clip_width(), 1.0); ctx.rectangle(x.into(), y.into(), self.clip_width(), 1.0);
let (r,g,b,a) = if selected { self.theme.result_bg_selected } if selected { ctx.theme_color_highlight(&self.theme) }
else { self.theme.result_bg }; else { ctx.theme_color_base(&self.theme) };
ctx.set_source_rgba(r,g,b,a); ctx.set_operator(cairo::Operator::Source);
ctx.fill().unwrap(); ctx.fill().unwrap();
// draw text // 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); ctx.move_to (x+0.2, y+0.8);
ctx.theme_text(&result, selected, indices, &self.theme)
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();
}
}
} }
pub fn divider<T>(&self, x: T, y: T) pub fn divider<T>(&self, x: T, y: T)
@ -141,18 +92,13 @@ impl<'a> Painter<'a> {
let ctx = self.context; let ctx = self.context;
let (x, y): (f64, f64) = (x.into(), y.into()); let (x, y): (f64, f64) = (x.into(), y.into());
ctx.theme_divider(&self.theme);
ctx.move_to(x, y); 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.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.stroke().unwrap();
ctx.rectangle(x, y+dash_width, self.clip_width(), self.clip_height()-dash_width); // ctx.rectangle(x, y+dash_width, self.clip_width(), self.clip_height()-dash_width);
ctx.clip(); // ctx.clip();
} }
fn device_to_user(&self, point: f64) -> f64 { fn device_to_user(&self, point: f64) -> f64 {

125
src/ui/theme.rs Normal file
View file

@ -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")
}
}