use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
use super::text::{fill_rect, DWrite};
use crate::event::Rect;
use crate::primitives::form::{Form, FormLayout};
use crate::primitives::toolbar::ToolbarButton;
use crate::theme::Theme;
use crate::types::WidgetId;
pub(crate) fn toolbar_item(field_id: &WidgetId, btn: &ToolbarButton) -> Option<(WidgetId, String)> {
match btn {
ToolbarButton::Action { id, label, .. } => Some((*id, label.clone())),
ToolbarButton::Label { text, .. } => Some((*field_id, text.clone())),
ToolbarButton::Separator => None,
}
}
pub fn win_form_layout(dwrite: &DWrite, rect: Rect, form: &Form, line_height: f32) -> FormLayout {
let row_h = crate::primitives::layout_metrics::form_row_height(line_height as f64);
form.layout(rect.width, rect.height, |i| {
crate::primitives::layout_metrics::form_field_measure(&form.fields[i], row_h, dwrite)
})
}
const SETTINGS_CURSOR_W: f32 = 1.5;
const SETTINGS_SEARCH_PREFIX: &str = " / ";
#[allow(clippy::too_many_arguments)]
pub fn draw_settings_chrome(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
line_height: f32,
header_text: &str,
query: &str,
placeholder: &str,
active: bool,
) {
if rect.width <= 0.0 || line_height <= 0.0 {
return;
}
let theme = Theme::default();
let _ = fill_rect(
target,
Rect::new(rect.x, rect.y, rect.width, line_height),
theme.header_bg,
);
let (header_w, header_h) = dwrite.measure_text(header_text).unwrap_or((0.0, 0.0));
let header_y = rect.y + (line_height - header_h) / 2.0;
let _ = dwrite.draw_text(
target,
header_text,
Rect::new(rect.x + 2.0, header_y, header_w, header_h),
theme.header_fg,
);
if rect.height >= line_height * 1.5 {
let search_y = rect.y + line_height;
let row_bg = if active {
theme.selected_bg
} else {
theme.tab_bar_bg
};
let _ = fill_rect(
target,
Rect::new(rect.x, search_y, rect.width, line_height),
row_bg,
);
let (prefix_w, prefix_h) = dwrite
.measure_text(SETTINGS_SEARCH_PREFIX)
.unwrap_or((0.0, 0.0));
let prefix_y = search_y + (line_height - prefix_h) / 2.0;
let _ = dwrite.draw_text(
target,
SETTINGS_SEARCH_PREFIX,
Rect::new(rect.x + 2.0, prefix_y, prefix_w, prefix_h),
theme.muted_fg,
);
let q_x = rect.x + 2.0 + prefix_w;
let show_placeholder = query.is_empty() && !placeholder.is_empty() && !active;
let (text, color) = if show_placeholder {
(placeholder, theme.muted_fg)
} else if query.is_empty() {
(query, theme.muted_fg)
} else {
(query, theme.foreground)
};
let (text_w, text_h) = dwrite.measure_text(text).unwrap_or((0.0, 0.0));
let text_y = search_y + (line_height - text_h) / 2.0;
let _ = dwrite.draw_text(target, text, Rect::new(q_x, text_y, text_w, text_h), color);
if active {
let (query_w, _) = dwrite.measure_text(query).unwrap_or((0.0, 0.0));
let cur_x = q_x + if query.is_empty() { 0.0 } else { query_w };
let _ = fill_rect(
target,
Rect::new(cur_x, search_y + 2.0, SETTINGS_CURSOR_W, line_height - 4.0),
theme.accent_fg,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::form::{FieldKind, FormHit, ToggleGroupItem};
use crate::types::{Color, StyledText};
use crate::win::testing::HeadlessSurface;
const W: f32 = 300.0;
const H: f32 = 200.0;
const LINE_HEIGHT: f32 = 14.0;
fn paint(surface: &HeadlessSurface, dwrite: &DWrite, rect: Rect, form: &Form) -> FormLayout {
let layout = win_form_layout(dwrite, rect, form, LINE_HEIGHT);
let theme = Theme::default();
let origin = crate::Point::new(rect.x, rect.y);
surface
.paint(|target| {
let mut raw = super::super::surface::D2dSurface {
target,
dwrite: Some(dwrite),
};
crate::primitives::form::paint(form, &layout, &mut raw, &theme, origin);
})
.expect("paint form");
layout
}
fn field(id: &str, label: &str, kind: FieldKind) -> crate::primitives::form::FormField {
crate::primitives::form::FormField {
id: WidgetId::new(id),
label: StyledText::plain(label.to_string()),
kind,
hint: StyledText::plain(""),
disabled: false,
validation: None,
}
}
fn make_form(fields: Vec<crate::primitives::form::FormField>) -> Form {
Form {
id: WidgetId::new("form"),
fields,
focused_field: None,
scroll_offset: 0,
has_focus: true,
}
}
#[test]
fn paint_and_hit_test_round_trip() {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let form = make_form(vec![
field(
"name",
"Name",
FieldKind::TextInput {
value: "quadraui".into(),
placeholder: String::new(),
cursor: Some(4),
selection_anchor: None,
},
),
field(
"flags",
"Flags",
FieldKind::ToggleGroup {
toggles: vec![
ToggleGroupItem {
id: WidgetId::new("flags:a"),
label: "A".into(),
value: true,
},
ToggleGroupItem {
id: WidgetId::new("flags:b"),
label: "B".into(),
value: false,
},
],
},
),
]);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = paint(&surface, &dwrite, rect, &form);
assert_eq!(layout.visible_fields.len(), 2);
for vf in &layout.visible_fields {
let cx = vf.bounds.x + 1.0;
let cy = vf.bounds.y + vf.bounds.height / 2.0;
assert_eq!(layout.hit_test(cx, cy), FormHit::Field(vf.id));
}
let toggle_field = &layout.visible_fields[1];
assert_eq!(toggle_field.item_bounds.len(), 2);
for (id, item_rect) in &toggle_field.item_bounds {
let cx = item_rect.x + item_rect.width / 2.0;
let cy = item_rect.y + item_rect.height / 2.0;
assert_eq!(layout.hit_test(cx, cy), FormHit::Field(*id));
}
}
#[test]
fn focused_row_paints_selected_bg() {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let mut form = make_form(vec![
field("a", "A", FieldKind::Toggle { value: false }),
field("b", "B", FieldKind::Toggle { value: true }),
]);
form.focused_field = Some(WidgetId::new("b"));
let rect = Rect::new(0.0, 0.0, W, H);
let layout = paint(&surface, &dwrite, rect, &form);
let theme = Theme::default();
let bounds = layout.visible_fields[1].bounds;
let px = surface.pixel_at((bounds.x + 1.0) as u32, (bounds.y + 1.0) as u32);
assert_eq!(
(px.r, px.g, px.b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
)
);
}
#[test]
fn paint_with_cursors_does_not_panic() {
let form = make_form(vec![
field(
"name",
"Name",
FieldKind::TextInput {
value: "hi".into(),
placeholder: String::new(),
cursor: None,
selection_anchor: None,
},
),
field(
"pw",
"Password",
FieldKind::PasswordInput {
value: "secret".into(),
placeholder: String::new(),
cursor: Some(3),
mask_char: '*',
},
),
]);
let rect = Rect::new(0.0, 0.0, W, H);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let _ = paint(&surface, &dwrite, rect, &form);
}
#[test]
fn settings_chrome_paints_header_and_inactive_search_rows() {
let surface = HeadlessSurface::new(200, 60).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let rect = Rect::new(0.0, 0.0, 200.0, 60.0);
let line_height = 18.0_f32;
surface
.paint(|target| {
draw_settings_chrome(
target,
&dwrite,
rect,
line_height,
"Settings",
"",
"search…",
false,
);
})
.expect("paint settings chrome");
let theme = Theme::default();
let header_px = surface.pixel_at(150, 2);
assert_eq!(
(header_px.r, header_px.g, header_px.b),
(theme.header_bg.r, theme.header_bg.g, theme.header_bg.b),
"header row paints header_bg"
);
let search_px = surface.pixel_at(150, line_height as u32 + 2);
assert_eq!(
(search_px.r, search_px.g, search_px.b),
(theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b),
"inactive search row paints tab_bar_bg"
);
}
#[test]
fn settings_chrome_active_search_row_paints_selected_bg() {
let surface = HeadlessSurface::new(200, 60).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let rect = Rect::new(0.0, 0.0, 200.0, 60.0);
let line_height = 18.0_f32;
surface
.paint(|target| {
draw_settings_chrome(
target,
&dwrite,
rect,
line_height,
"Settings",
"op",
"",
true,
);
})
.expect("paint settings chrome");
let theme = Theme::default();
let search_px = surface.pixel_at(150, line_height as u32 + 2);
assert_eq!(
(search_px.r, search_px.g, search_px.b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"active search row paints selected_bg"
);
}
#[test]
fn settings_chrome_one_row_height_paints_no_search_row() {
let surface = HeadlessSurface::new(200, 60).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let line_height = 18.0_f32;
let sentinel = Color::rgb(1, 2, 3);
surface
.fill_rect(Rect::new(0.0, 0.0, 200.0, 60.0), sentinel)
.expect("fill sentinel");
let one_row = Rect::new(0.0, 0.0, 200.0, line_height);
surface
.paint(|target| {
draw_settings_chrome(
target,
&dwrite,
one_row,
line_height,
"HEADER",
"",
"placeholder",
false,
);
})
.expect("paint settings chrome");
let theme = Theme::default();
let header_px = surface.pixel_at(150, 2);
assert_eq!(
(header_px.r, header_px.g, header_px.b),
(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"
);
let search_px = surface.pixel_at(150, line_height as u32 + 2);
assert_eq!(
(search_px.r, search_px.g, search_px.b),
(sentinel.r, sentinel.g, sentinel.b),
"a 1-row-tall chrome rect must not paint a search row past its own height"
);
}
}