use gtk4::cairo::Context;
use gtk4::pango;
use super::cairo_rgb;
use crate::theme::Theme;
#[allow(clippy::too_many_arguments)]
pub fn draw_settings_chrome(
cr: &Context,
layout: &pango::Layout,
x: f64,
y: f64,
w: f64,
height: f64,
line_height: f64,
header_text: &str,
query: &str,
placeholder: &str,
active: bool,
theme: &Theme,
) {
if w <= 0.0 || line_height <= 0.0 {
return;
}
let bg = cairo_rgb(theme.tab_bar_bg);
let hdr_bg = cairo_rgb(theme.header_bg);
let hdr_fg = cairo_rgb(theme.header_fg);
let fg = cairo_rgb(theme.foreground);
let dim = cairo_rgb(theme.muted_fg);
let sel = cairo_rgb(theme.selected_bg);
let accent = cairo_rgb(theme.accent_fg);
layout.set_attributes(None);
cr.set_source_rgb(hdr_bg.0, hdr_bg.1, hdr_bg.2);
cr.rectangle(x, y, w, line_height);
cr.fill().ok();
cr.set_source_rgb(hdr_fg.0, hdr_fg.1, hdr_fg.2);
layout.set_text(header_text);
let (_, header_lh) = layout.pixel_size();
cr.move_to(
x + 2.0,
(y + (line_height - header_lh as f64) / 2.0).round(),
);
super::painted_text::show_layout(cr, layout);
if height >= line_height * 1.5 {
let search_y = y + line_height;
let (sb_r, sb_g, sb_b) = if active { sel } else { bg };
cr.set_source_rgb(sb_r, sb_g, sb_b);
cr.rectangle(x, search_y, w, line_height);
cr.fill().ok();
let prefix = " / ";
cr.set_source_rgb(dim.0, dim.1, dim.2);
layout.set_text(prefix);
let (prefix_w, _) = layout.pixel_size();
cr.move_to(
x + 2.0,
(search_y + (line_height - header_lh as f64) / 2.0).round(),
);
super::painted_text::show_layout(cr, layout);
let q_x = x + 2.0 + prefix_w as f64;
let show_placeholder = query.is_empty() && !placeholder.is_empty() && !active;
let (text, color) = if show_placeholder {
(placeholder, dim)
} else if query.is_empty() {
(query, dim)
} else {
(query, fg)
};
cr.set_source_rgb(color.0, color.1, color.2);
layout.set_text(text);
let (q_w, _) = layout.pixel_size();
cr.move_to(
q_x,
(search_y + (line_height - header_lh as f64) / 2.0).round(),
);
super::painted_text::show_layout(cr, layout);
if active {
let cur_x = q_x + if query.is_empty() { 0.0 } else { q_w as f64 };
cr.set_source_rgb(accent.0, accent.1, accent.2);
cr.rectangle(cur_x, search_y + 2.0, 1.5, line_height - 4.0);
cr.fill().ok();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gtk::toolbar::PangoMeasure;
use crate::primitives::form::{FieldKind, Form, FormField};
use crate::types::{StyledText, WidgetId};
use pangocairo::cairo::{Context, Format, ImageSurface};
fn paint(form: &Form) {
let surface = ImageSurface::create(Format::ARgb32, 320, 160).expect("create ImageSurface");
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let theme = Theme::default();
let row_h = crate::primitives::layout_metrics::form_row_height(14.0);
let measure = PangoMeasure {
pango_layout: Some(&pango_layout),
char_width: 8.0,
};
let flayout = form.layout(320.0, 160.0, |i| {
crate::primitives::layout_metrics::form_field_measure(&form.fields[i], row_h, &measure)
});
let mut raw = crate::gtk::surface::CairoSurface {
cr: &cr,
layout: Some(&pango_layout),
translucent_fill: false,
};
crate::primitives::form::paint(
form,
&flayout,
&mut raw,
&theme,
crate::Point::new(0.0, 0.0),
);
}
fn pixel(data: &[u8], stride: usize, x: i32, y: i32) -> (u8, u8, u8) {
let off = y as usize * stride + x as usize * 4;
(data[off + 2], data[off + 1], data[off])
}
#[test]
fn draw_form_clamps_out_of_range_scroll_offset_like_form_layout() {
let form = Form {
id: WidgetId::new("settings"),
fields: vec![
FormField {
id: WidgetId::new("hdr"),
label: StyledText::plain("Header"),
kind: FieldKind::Label,
hint: StyledText::default(),
disabled: false,
validation: None,
},
FormField {
id: WidgetId::new("enabled"),
label: StyledText::plain("Enabled"),
kind: FieldKind::Toggle { value: true },
hint: StyledText::default(),
disabled: false,
validation: None,
},
],
focused_field: Some(WidgetId::new("enabled")),
scroll_offset: 50, has_focus: true,
};
let mut surface =
ImageSurface::create(Format::ARgb32, 320, 160).expect("create ImageSurface");
{
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let theme = Theme::default();
let row_h = crate::primitives::layout_metrics::form_row_height(14.0);
let measure = PangoMeasure {
pango_layout: Some(&pango_layout),
char_width: 8.0,
};
let flayout = form.layout(320.0, 160.0, |i| {
crate::primitives::layout_metrics::form_field_measure(
&form.fields[i],
row_h,
&measure,
)
});
let mut raw = crate::gtk::surface::CairoSurface {
cr: &cr,
layout: Some(&pango_layout),
translucent_fill: false,
};
crate::primitives::form::paint(
&form,
&flayout,
&mut raw,
&theme,
crate::Point::new(0.0, 0.0),
);
}
let theme = Theme::default();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let (r, g, b) = pixel(&data, stride, 4, 4);
assert_eq!(
(r, g, b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"row 0 should paint the focused 'enabled' field (the field \
`clamp_scroll_offset` resolves to), not leave the background \
untouched the way the pre-fix raw `.skip()` did",
);
}
#[test]
fn text_input_with_multibyte_cursor_and_selection_does_not_panic() {
let value = "café🎉";
assert!(!value.is_char_boundary(4));
assert!(!value.is_char_boundary(7));
let form = Form {
id: WidgetId::new("settings"),
fields: vec![FormField {
id: WidgetId::new("name"),
label: StyledText::plain("Name"),
kind: FieldKind::TextInput {
value: value.into(),
placeholder: String::new(),
cursor: Some(4),
selection_anchor: Some(7),
},
hint: StyledText::default(),
disabled: false,
validation: None,
}],
focused_field: Some(WidgetId::new("name")),
scroll_offset: 0,
has_focus: true,
};
paint(&form);
}
#[test]
fn password_input_with_multibyte_cursor_does_not_panic() {
let value = "café🎉";
assert!(!value.is_char_boundary(4));
let form = Form {
id: WidgetId::new("settings"),
fields: vec![FormField {
id: WidgetId::new("pw"),
label: StyledText::plain("Password"),
kind: FieldKind::PasswordInput {
value: value.into(),
placeholder: String::new(),
cursor: Some(4),
mask_char: '*',
},
hint: StyledText::default(),
disabled: false,
validation: None,
}],
focused_field: Some(WidgetId::new("pw")),
scroll_offset: 0,
has_focus: true,
};
paint(&form);
}
#[test]
fn text_area_with_multibyte_cursor_does_not_panic() {
let value = "café🎉\nsecond line";
assert!(!value.is_char_boundary(4));
let form = Form {
id: WidgetId::new("settings"),
fields: vec![FormField {
id: WidgetId::new("notes"),
label: StyledText::plain("Notes"),
kind: FieldKind::TextArea {
value: value.into(),
placeholder: String::new(),
cursor: Some(4),
visible_rows: 3,
},
hint: StyledText::default(),
disabled: false,
validation: None,
}],
focused_field: Some(WidgetId::new("notes")),
scroll_offset: 0,
has_focus: true,
};
paint(&form);
}
const CHROME_W: i32 = 200;
const CHROME_H: i32 = 60;
const LINE_HEIGHT: f64 = 18.0;
const SENTINEL: (u8, u8, u8) = (1, 2, 3);
fn paint_chrome(height: f64) -> (Vec<u8>, usize) {
let mut surface =
ImageSurface::create(Format::ARgb32, CHROME_W, CHROME_H).expect("create ImageSurface");
{
let cr = Context::new(&surface).expect("Context::new");
let (sr, sg, sb) = (
SENTINEL.0 as f64 / 255.0,
SENTINEL.1 as f64 / 255.0,
SENTINEL.2 as f64 / 255.0,
);
cr.set_source_rgb(sr, sg, sb);
cr.paint().ok();
let pango_layout = pangocairo::functions::create_layout(&cr);
let theme = Theme::default();
draw_settings_chrome(
&cr,
&pango_layout,
0.0,
0.0,
CHROME_W as f64,
height,
LINE_HEIGHT,
"HEADER",
"",
"placeholder",
false,
&theme,
);
}
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data").to_vec();
(data, stride)
}
#[test]
fn draw_settings_chrome_one_row_height_paints_no_search_row() {
let (data, stride) = paint_chrome(LINE_HEIGHT);
let probe_x = 150;
let probe_y = (LINE_HEIGHT * 1.5) as i32;
assert_eq!(
pixel(&data, stride, probe_x, probe_y),
SENTINEL,
"a 1-row-tall chrome rect must not paint a search row past its own height"
);
let theme = Theme::default();
assert_eq!(
pixel(&data, stride, probe_x, (LINE_HEIGHT / 2.0) as i32),
(theme.header_bg.r, theme.header_bg.g, theme.header_bg.b),
"the header row must still paint even when the search row doesn't"
);
}
#[test]
fn draw_settings_chrome_two_row_height_paints_search_row() {
let (data, stride) = paint_chrome(LINE_HEIGHT * 2.0);
let theme = Theme::default();
let probe_y = (LINE_HEIGHT * 1.5) as i32;
assert_eq!(
pixel(&data, stride, 4, probe_y),
(theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b),
"a 2-row-tall chrome rect must still paint the inactive search row"
);
}
}