use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
use super::text::{fill_rect, stroke_rect, DWrite};
use crate::event::Rect;
use crate::primitives::text_input::{TextInput, TextInputLayout, TextInputMeasure};
use crate::theme::Theme;
const CURSOR_W_DIP: f32 = 2.0;
pub fn win_text_input_layout(
ti: &TextInput,
rect: Rect,
line_height: f32,
char_width: f32,
) -> TextInputLayout {
ti.layout(rect, TextInputMeasure::new(line_height, char_width))
}
#[allow(clippy::too_many_arguments)]
pub fn draw_text_input(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
ti: &TextInput,
theme: &Theme,
line_height: f32,
char_width: f32,
) -> TextInputLayout {
let layout = win_text_input_layout(ti, rect, line_height, char_width);
if rect.width <= 0.0 || rect.height <= 0.0 {
return layout;
}
let _ = fill_rect(target, rect, theme.background);
let border_color = if ti.has_focus {
theme.accent_fg
} else {
theme.border_fg
};
let _ = stroke_rect(target, rect, border_color, 1.0);
let h_scroll = layout.resolved_scroll_col;
let slice_from = |line: &str, off: usize| -> String {
if off == 0 {
line.to_string()
} else {
line.chars().skip(off).collect()
}
};
if layout.placeholder_active {
if let (Some(text), Some(first)) = (ti.placeholder.as_ref(), layout.visible_lines.first()) {
let _ = dwrite.draw_text(target, text, first.bounds, theme.muted_fg);
}
} else {
for vis in &layout.visible_lines {
let full = ti.lines.get(vis.line_idx).map(String::as_str).unwrap_or("");
let visible = slice_from(full, h_scroll);
if visible.is_empty() {
continue;
}
let _ = dwrite.draw_text(target, &visible, vis.bounds, theme.foreground);
}
}
if ti.has_focus {
if let Some(cb) = layout.cursor_bounds {
let cursor_rect = Rect::new(cb.x, cb.y, CURSOR_W_DIP, cb.height);
let _ = fill_rect(target, cursor_rect, theme.cursor);
}
}
layout
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::text_input::TextInputHit;
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: f32 = 200.0;
const H: f32 = 60.0;
fn sample(lines: Vec<&str>, cursor_line: usize, cursor_col: usize) -> TextInput {
let mut ti = TextInput::new(WidgetId::new("ti"));
ti.lines = lines.into_iter().map(String::from).collect();
ti.cursor_line = cursor_line;
ti.cursor_col = cursor_col;
ti.has_focus = true;
ti
}
#[test]
fn draw_text_input_paints_and_does_not_panic_on_multibyte_cursor() {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, char_width) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let theme = Theme {
background: Color::rgb(255, 255, 255),
foreground: Color::rgb(0, 0, 0),
..Theme::default()
};
let ti = sample(vec![":éditer"], 0, 2);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = surface
.paint(|target| {
draw_text_input(target, &dwrite, rect, &ti, &theme, 16.0, char_width);
})
.map(|_| win_text_input_layout(&ti, rect, 16.0, char_width))
.expect("paint text input");
let mut painted_any = false;
for x in (layout.content_bounds.x as u32)..(layout.content_bounds.x + 40.0) as u32 {
for y in (layout.content_bounds.y as u32)..(layout.content_bounds.y + 16.0) as u32 {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != (255, 255, 255) {
painted_any = true;
}
}
}
assert!(painted_any, "expected text_input to paint visible glyphs");
}
#[test]
fn paint_and_click_round_trip_at_nonzero_origin() {
let origin_x = 12.0_f32;
let origin_y = 5.0_f32;
let (dwrite, _, char_width) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let theme = Theme::default();
let ti = sample(vec!["hello", "world"], 0, 0);
let rect = Rect::new(origin_x, origin_y, W - origin_x, H - origin_y);
let layout = surface
.paint(|target| {
draw_text_input(target, &dwrite, rect, &ti, &theme, 16.0, char_width);
})
.map(|_| win_text_input_layout(&ti, rect, 16.0, char_width))
.expect("paint text input");
assert!(layout.content_bounds.x >= origin_x);
assert!(layout.content_bounds.y >= origin_y);
let first_line = layout.visible_lines.first().expect("one visible line");
let hit = layout
.hit_regions
.iter()
.find(|(r, _)| {
r.contains(crate::event::Point::new(
first_line.bounds.x + 0.5,
first_line.bounds.y + 0.5,
))
})
.map(|(_, h)| h.clone());
assert_eq!(hit, Some(TextInputHit::Line { line_idx: 0 }));
}
#[test]
fn cursor_paints_at_the_shared_layout_bounds() {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, char_width) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let theme = Theme {
cursor: Color::rgb(255, 0, 0),
..Theme::default()
};
let ti = sample(vec!["hello"], 0, 3);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = win_text_input_layout(&ti, rect, 16.0, char_width);
let cb = layout.cursor_bounds.expect("cursor visible");
surface
.paint(|target| {
draw_text_input(target, &dwrite, rect, &ti, &theme, 16.0, char_width);
})
.expect("paint text input");
let px = (cb.x + CURSOR_W_DIP / 2.0) as u32;
let py = (cb.y + cb.height / 2.0) as u32;
let sample_px = surface.pixel_at(px, py);
assert_eq!(
(sample_px.r, sample_px.g, sample_px.b),
(theme.cursor.r, theme.cursor.g, theme.cursor.b),
"insert cursor should paint at cursor_bounds",
);
}
#[test]
fn zero_width_rect_is_a_no_op() {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, char_width) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let theme = Theme::default();
let ti = sample(vec!["hello"], 0, 0);
let rect = Rect::new(0.0, 0.0, 0.0, H);
surface
.fill_rect(Rect::new(0.0, 0.0, W, H), Color::rgb(255, 255, 255))
.expect("fill background");
surface
.paint(|target| {
draw_text_input(target, &dwrite, rect, &ti, &theme, 16.0, char_width);
})
.expect("paint text input");
let px = surface.pixel_at(1, 1);
assert_eq!(
(px.r, px.g, px.b),
(255, 255, 255),
"a zero-width text input should paint nothing at all",
);
}
#[test]
fn no_paint_layout_matches_paint_layout() {
let ti = sample(vec!["hello", "world"], 1, 2);
let rect = Rect::new(0.0, 0.0, W, H);
let (dwrite, _, char_width) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let painted = surface
.paint(|target| {
draw_text_input(
target,
&dwrite,
rect,
&ti,
&Theme::default(),
16.0,
char_width,
);
})
.map(|_| win_text_input_layout(&ti, rect, 16.0, char_width))
.expect("paint");
let no_paint = win_text_input_layout(&ti, rect, 16.0, char_width);
assert_eq!(painted, no_paint);
}
#[test]
fn win_text_input_layout_delegates_to_shared_primitive_layout() {
let ti = sample(vec!["hello", "world"], 1, 3);
let rect = Rect::new(4.0, 6.0, W, H);
let line_height = 18.0;
let char_width = 9.0;
let via_wrapper = win_text_input_layout(&ti, rect, line_height, char_width);
let direct = ti.layout(rect, TextInputMeasure::new(line_height, char_width));
assert_eq!(via_wrapper, direct);
}
}