use crate::event::Rect;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FindReplaceClickTarget {
Chevron,
FindInput(usize),
ReplaceInput(usize),
ToggleCase,
ToggleWholeWord,
ToggleRegex,
PrevMatch,
NextMatch,
ToggleInSelection,
Close,
TogglePreserveCase,
ReplaceCurrent,
ReplaceAll,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrHitRegion {
pub col: u16,
pub row: u16,
pub width: u16,
}
pub const FR_PANEL_WIDTH: u16 = 50;
pub fn compute_hit_regions(
panel_w: u16,
show_replace: bool,
match_info: &str,
replace_one_glyph_width: u16,
replace_all_glyph_width: u16,
) -> (Vec<(FrHitRegion, FindReplaceClickTarget)>, u16) {
use FindReplaceClickTarget::*;
let mut regions = Vec::with_capacity(16);
let content_w = panel_w.saturating_sub(2);
regions.push((
FrHitRegion {
col: 0,
row: 0,
width: 2,
},
Chevron,
));
let info_len = (match_info.len() as u16).max(5);
let right_side_w: u16 = 9 + info_len + 1 + 8; let input_start: u16 = 2;
let input_w = content_w.saturating_sub(2 + right_side_w);
regions.push((
FrHitRegion {
col: input_start,
row: 0,
width: input_w,
},
FindInput(0),
));
let mut tx = input_start + input_w + 1;
for target in [ToggleCase, ToggleWholeWord, ToggleRegex] {
regions.push((
FrHitRegion {
col: tx,
row: 0,
width: 2,
},
target,
));
tx += 3;
}
tx += info_len + 1;
for target in [PrevMatch, NextMatch, ToggleInSelection, Close] {
regions.push((
FrHitRegion {
col: tx,
row: 0,
width: 2,
},
target,
));
tx += 2;
}
if show_replace {
regions.push((
FrHitRegion {
col: input_start,
row: 1,
width: input_w,
},
ReplaceInput(0),
));
let mut bx = input_start + input_w + 1;
regions.push((
FrHitRegion {
col: bx,
row: 1,
width: 2,
},
TogglePreserveCase,
));
bx += 3;
regions.push((
FrHitRegion {
col: bx,
row: 1,
width: replace_one_glyph_width,
},
ReplaceCurrent,
));
bx += replace_one_glyph_width + 1;
regions.push((
FrHitRegion {
col: bx,
row: 1,
width: replace_all_glyph_width,
},
ReplaceAll,
));
}
(regions, input_w)
}
#[derive(Debug, Clone)]
pub struct FindReplacePanel {
pub query: String,
pub replacement: String,
pub show_replace: bool,
pub focus: u8,
pub cursor: usize,
pub sel_anchor: Option<usize>,
pub match_info: String,
pub case_sensitive: bool,
pub whole_word: bool,
pub use_regex: bool,
pub preserve_case: bool,
pub in_selection: bool,
pub group_bounds: Rect,
pub panel_width: u16,
pub replace_one_glyph: String,
pub replace_all_glyph: String,
pub hit_regions: Vec<(FrHitRegion, FindReplaceClickTarget)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FindReplaceHit {
Target(FindReplaceClickTarget),
Empty,
}
impl FindReplacePanel {
pub fn hit_test(
&self,
x: f32,
y: f32,
content_origin: (f32, f32),
char_width: f32,
line_height: f32,
) -> FindReplaceHit {
if char_width <= 0.0 || line_height <= 0.0 {
return FindReplaceHit::Empty;
}
let (ox, oy) = content_origin;
if x < ox || y < oy {
return FindReplaceHit::Empty;
}
for (region, target) in &self.hit_regions {
let rx0 = ox + region.col as f32 * char_width;
let rx1 = rx0 + region.width as f32 * char_width;
let ry0 = oy + region.row as f32 * line_height;
let ry1 = ry0 + line_height;
if x >= rx0 && x < rx1 && y >= ry0 && y < ry1 {
return FindReplaceHit::Target(*target);
}
}
FindReplaceHit::Empty
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
mod native_surface_paint {
use super::{FindReplaceClickTarget, FindReplacePanel};
use crate::paint_surface::PaintSurface;
use crate::theme::Theme;
use crate::types::Color;
use crate::Rect;
struct Metrics {
cw: f32,
lh: f32,
content_x: f32,
content_y: f32,
}
fn region_rect(m: &Metrics, col: u16, row: u16, width: u16) -> Rect {
Rect::new(
m.content_x + col as f32 * m.cw,
m.content_y + row as f32 * m.lh,
(width as f32 * m.cw).max(1.0),
m.lh,
)
}
fn paint_label(
surface: &mut dyn PaintSurface,
m: &Metrics,
col: u16,
row: u16,
text: &str,
fg: Color,
) {
let (tw, _) = surface.surface_measure_text(text);
let px = m.content_x + col as f32 * m.cw;
let py = m.content_y + row as f32 * m.lh;
surface.surface_draw_text_run(Rect::new(px, py, tw, m.lh), text, fg);
}
#[allow(clippy::too_many_arguments)]
fn paint_toggle(
surface: &mut dyn PaintSurface,
theme: &Theme,
m: &Metrics,
col: u16,
row: u16,
width: u16,
label: &str,
active: bool,
) {
let rect = region_rect(m, col, row, width);
let fg = if active {
surface.surface_fill_rect(rect, theme.accent_bg);
theme.background
} else {
surface.surface_stroke_rect(rect, theme.separator, 0.5);
theme.foreground
};
let (tw, _) = surface.surface_measure_text(label);
let tx = rect.x + ((rect.width - tw) / 2.0).max(0.0);
surface.surface_draw_text_run(Rect::new(tx, rect.y, tw, m.lh), label, fg);
}
#[allow(clippy::too_many_arguments)]
fn paint_glyph(
surface: &mut dyn PaintSurface,
theme: &Theme,
m: &Metrics,
col: u16,
row: u16,
width: u16,
label: &str,
active: bool,
) {
let rect = region_rect(m, col, row, width);
let fg = if active {
surface.surface_fill_rect(rect, theme.accent_bg);
theme.background
} else {
theme.foreground
};
let (tw, _) = surface.surface_measure_text(label);
let tx = rect.x + ((rect.width - tw) / 2.0).max(0.0);
surface.surface_draw_text_run(Rect::new(tx, rect.y, tw, m.lh), label, fg);
}
#[allow(clippy::too_many_arguments)]
fn paint_input(
surface: &mut dyn PaintSurface,
theme: &Theme,
m: &Metrics,
col: u16,
row: u16,
width: u16,
text: &str,
is_focused: bool,
cursor: usize,
sel_anchor: Option<usize>,
) {
let rect = region_rect(m, col, row, width);
surface.surface_fill_rect(rect, theme.background);
surface.surface_stroke_rect(rect, theme.separator, 0.5);
let text_x = rect.x + 4.0;
let has_sel = is_focused && sel_anchor.is_some_and(|a| a != cursor) && !text.is_empty();
if !has_sel {
let (tw, _) = surface.surface_measure_text(text);
surface.surface_draw_text_run(
Rect::new(text_x, rect.y, tw, m.lh),
text,
theme.foreground,
);
} else {
let anchor = sel_anchor.expect("has_sel implies sel_anchor is Some");
let (lo, hi) = (anchor.min(cursor), anchor.max(cursor));
let lo_b = crate::text_util::char_to_byte_idx(text, lo);
let hi_b = crate::text_util::char_to_byte_idx(text, hi);
let prefix = &text[..lo_b];
let selected = &text[lo_b..hi_b];
let suffix = &text[hi_b..];
let (pw, _) = surface.surface_measure_text(prefix);
let (sw, _) = surface.surface_measure_text(selected);
let (suw, _) = surface.surface_measure_text(suffix);
surface.surface_fill_rect(
Rect::new(text_x + pw, rect.y, sw.max(1.0), m.lh),
theme.selection_bg,
);
surface.surface_draw_text_run(
Rect::new(text_x, rect.y, pw, m.lh),
prefix,
theme.foreground,
);
surface.surface_draw_text_run(
Rect::new(text_x + pw, rect.y, sw, m.lh),
selected,
theme.foreground,
);
surface.surface_draw_text_run(
Rect::new(text_x + pw + sw, rect.y, suw, m.lh),
suffix,
theme.foreground,
);
}
if !is_focused {
return;
}
let cursor_b = crate::text_util::char_to_byte_idx(text, cursor);
let (cursor_px, _) = surface.surface_measure_text(&text[..cursor_b]);
surface.surface_fill_rect(
Rect::new(text_x + cursor_px, rect.y + 2.0, 2.0, (m.lh - 4.0).max(1.0)),
theme.foreground,
);
}
pub(crate) fn paint(panel: &FindReplacePanel, surface: &mut dyn PaintSurface, theme: &Theme) {
use FindReplaceClickTarget as T;
let cw = surface.surface_char_width().max(1.0);
let lh = surface.surface_line_height().max(1.0);
let popup_w = panel.panel_width as f32 * cw;
let row_count = if panel.show_replace { 2.0 } else { 1.0 };
let popup_h = (row_count + 2.0) * lh;
let gb = &panel.group_bounds;
let popup_x = ((gb.x + gb.width) - popup_w - 10.0).max(gb.x);
let popup_y = gb.y + 2.0;
let popup_rect = Rect::new(popup_x, popup_y, popup_w, popup_h);
surface.surface_fill_rect(popup_rect, theme.surface_bg);
surface.surface_stroke_rect(popup_rect, theme.separator, 1.0);
let m = Metrics {
cw,
lh,
content_x: popup_x + cw,
content_y: popup_y + lh,
};
let mut regex_end_col: Option<u16> = None;
let mut prev_match_col: Option<u16> = None;
for (region, target) in &panel.hit_regions {
match target {
T::Chevron => {
let chevron = if panel.show_replace {
"\u{25bc}"
} else {
"\u{25b6}"
};
paint_label(
surface,
&m,
region.col,
region.row,
chevron,
theme.foreground,
);
}
T::FindInput(_) => paint_input(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&panel.query,
panel.focus == 0,
panel.cursor,
panel.sel_anchor,
),
T::ReplaceInput(_) => paint_input(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&panel.replacement,
panel.focus == 1,
panel.cursor,
panel.sel_anchor,
),
T::ToggleCase => {
let label = crate::codicon::CASE_SENSITIVE.to_string();
paint_toggle(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&label,
panel.case_sensitive,
);
}
T::ToggleWholeWord => {
let label = crate::codicon::WHOLE_WORD.to_string();
paint_toggle(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&label,
panel.whole_word,
);
}
T::ToggleRegex => {
let label = crate::codicon::REGEX.to_string();
paint_toggle(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&label,
panel.use_regex,
);
regex_end_col = Some(region.col + region.width);
}
T::PrevMatch => {
paint_glyph(
surface,
theme,
&m,
region.col,
region.row,
region.width,
"\u{2191}",
false,
);
prev_match_col.get_or_insert(region.col);
}
T::NextMatch => paint_glyph(
surface,
theme,
&m,
region.col,
region.row,
region.width,
"\u{2193}",
false,
),
T::ToggleInSelection => paint_glyph(
surface,
theme,
&m,
region.col,
region.row,
region.width,
"\u{2261}",
panel.in_selection,
),
T::Close => paint_glyph(
surface,
theme,
&m,
region.col,
region.row,
region.width,
"\u{00d7}",
false,
),
T::TogglePreserveCase => {
let label = crate::codicon::PRESERVE_CASE.to_string();
paint_toggle(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&label,
panel.preserve_case,
);
}
T::ReplaceCurrent => paint_glyph(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&panel.replace_one_glyph,
false,
),
T::ReplaceAll => paint_glyph(
surface,
theme,
&m,
region.col,
region.row,
region.width,
&panel.replace_all_glyph,
false,
),
}
}
if let (Some(start_col), Some(end_col)) = (regex_end_col, prev_match_col) {
let info_col = start_col + 1;
if end_col > info_col + 1 {
paint_label(
surface,
&m,
info_col,
0,
&panel.match_info,
theme.foreground,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::ImagePaintResult;
use crate::event::Viewport;
use crate::primitives::find_replace::compute_hit_regions;
use crate::Image;
#[derive(Default)]
struct RecordingSurface {
fills: Vec<(Rect, Color)>,
strokes: Vec<(Rect, Color, f32)>,
text_runs: Vec<(Rect, String, Color)>,
}
impl PaintSurface for RecordingSurface {
fn surface_begin_frame(&mut self, _viewport: Viewport) {}
fn surface_end_frame(&mut self) {}
fn surface_viewport(&self) -> Viewport {
Viewport::new(600.0, 200.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
16.0
}
fn surface_char_width(&self) -> f32 {
8.0
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
(text.chars().count() as f32 * 8.0, 14.0)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
self.fills.push((rect, color));
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, _radius: f32, color: Color) {
self.fills.push((rect, color));
}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, stroke_width: f32) {
self.strokes.push((rect, color, stroke_width));
}
fn surface_stroke_rounded_rect(
&mut self,
rect: Rect,
_radius: f32,
color: Color,
stroke_width: f32,
) {
self.strokes.push((rect, color, stroke_width));
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: Color) {
self.text_runs.push((rect, text.to_string(), color));
}
fn surface_draw_line(
&mut self,
_from: crate::Point,
_to: crate::Point,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_push_clip(&mut self, _rect: Rect) {}
fn surface_pop_clip(&mut self) {}
fn surface_draw_image(&mut self, _rect: Rect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
fn sample_panel(query: &str, cursor: usize, sel_anchor: Option<usize>) -> FindReplacePanel {
let (hit_regions, _input_width) = compute_hit_regions(50, false, "1 of 3", 2, 2);
FindReplacePanel {
query: query.into(),
replacement: String::new(),
show_replace: false,
focus: 0,
cursor,
sel_anchor,
match_info: "1 of 3".into(),
case_sensitive: false,
whole_word: false,
use_regex: false,
preserve_case: false,
in_selection: false,
group_bounds: Rect::new(0.0, 0.0, 600.0, 200.0),
panel_width: 50,
replace_one_glyph: "R1".into(),
replace_all_glyph: "R*".into(),
hit_regions,
}
}
#[test]
fn paints_popup_background_and_border() {
let panel = sample_panel("needle", 6, None);
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&panel, &mut surface, &theme);
assert!(
surface.fills.iter().any(|(_, c)| *c == theme.surface_bg),
"expected a surface_bg fill for the popup background, fills were {:?}",
surface.fills,
);
assert!(
surface
.strokes
.iter()
.any(|(_, c, w)| *c == theme.separator && *w == 1.0),
"expected a 1.0-wide separator stroke for the popup border, strokes were {:?}",
surface.strokes,
);
}
#[test]
fn active_toggle_paints_accent_bg() {
let mut panel = sample_panel("needle", 6, None);
panel.case_sensitive = true;
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&panel, &mut surface, &theme);
assert!(
surface.fills.iter().any(|(_, c)| *c == theme.accent_bg),
"expected an accent_bg fill for the active toggle, fills were {:?}",
surface.fills,
);
}
#[test]
fn focused_input_selection_fills_and_still_draws_selected_text() {
let mut panel = sample_panel("needle", 3, Some(0));
panel.focus = 0;
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&panel, &mut surface, &theme);
assert!(
surface.fills.iter().any(|(_, c)| *c == theme.selection_bg),
"expected a selection_bg fill for the focused input's selection, fills were {:?}",
surface.fills,
);
assert!(
surface.text_runs.iter().any(|(_, t, _)| t == "nee"),
"selected substring \"nee\" must still be drawn as its own text run \
(the fill happens before text, so it can't hide it), text runs were {:?}",
surface.text_runs,
);
}
#[test]
fn toggle_buttons_paint_codicon_glyphs_not_literal_text() {
let mut panel = sample_panel("needle", 6, None);
panel.show_replace = true;
let (hit_regions, _input_width) = compute_hit_regions(50, true, "1 of 3", 2, 2);
panel.hit_regions = hit_regions.clone();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&panel, &mut surface, &theme);
let cw = surface.surface_char_width();
let popup_rect = surface
.fills
.first()
.map(|(r, _)| *r)
.expect("popup background fill recorded first");
let content_x = popup_rect.x + cw;
let toggles = [
(
FindReplaceClickTarget::ToggleCase,
crate::codicon::CASE_SENSITIVE,
),
(
FindReplaceClickTarget::ToggleWholeWord,
crate::codicon::WHOLE_WORD,
),
(FindReplaceClickTarget::ToggleRegex, crate::codicon::REGEX),
(
FindReplaceClickTarget::TogglePreserveCase,
crate::codicon::PRESERVE_CASE,
),
];
for (target, glyph) in toggles {
let (region, _) = hit_regions
.iter()
.find(|(_, t)| *t == target)
.unwrap_or_else(|| panic!("{target:?} region must exist"));
let label = glyph.to_string();
let (tw, _) = surface.surface_measure_text(&label);
let rect_width = region.width as f32 * cw;
let expected_x =
content_x + region.col as f32 * cw + ((rect_width - tw) / 2.0).max(0.0);
let run = surface
.text_runs
.iter()
.find(|(_, t, _)| t == &label)
.unwrap_or_else(|| {
panic!(
"expected codicon glyph {glyph:?} for {target:?} to be painted, \
text runs were {:?}",
surface.text_runs
)
});
assert!(
(run.0.x - expected_x).abs() < 0.01,
"{target:?}'s glyph {glyph:?} painted at x={}, expected x={} \
(its own region's projected rx0, not a neighbouring toggle's) — \
text runs were {:?}",
run.0.x,
expected_x,
surface.text_runs,
);
}
for stale in ["Aa", "ab", ".*", "AB"] {
assert!(
!surface.text_runs.iter().any(|(_, t, _)| t == stale),
"GUI toggle buttons must not paint the TUI-only literal {stale:?} \
text pair, text runs were {:?}",
surface.text_runs,
);
}
}
#[test]
fn unfocused_input_paints_no_selection_or_cursor() {
let mut panel = sample_panel("needle", 3, Some(0));
panel.focus = 1; let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&panel, &mut surface, &theme);
assert!(
!surface.fills.iter().any(|(_, c)| *c == theme.selection_bg),
"unfocused input must not paint a selection highlight, fills were {:?}",
surface.fills,
);
assert!(
surface.text_runs.iter().any(|(_, t, _)| t == "needle"),
"unfocused input must still paint its full text as one run, text runs were {:?}",
surface.text_runs,
);
}
#[test]
fn multibyte_query_with_out_of_range_selection_does_not_panic() {
let panel = sample_panel("café🎉中文", 3, Some(6));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&panel, &mut surface, &theme);
}
}
}
#[cfg(test)]
mod hit_test_tests {
use super::*;
fn panel(show_replace: bool) -> FindReplacePanel {
let (hit_regions, _input_width) = compute_hit_regions(50, show_replace, "1 of 3", 2, 2);
FindReplacePanel {
query: "needle".into(),
replacement: String::new(),
show_replace,
focus: 0,
cursor: 0,
sel_anchor: None,
match_info: "1 of 3".into(),
case_sensitive: false,
whole_word: false,
use_regex: false,
preserve_case: false,
in_selection: false,
group_bounds: Rect::new(0.0, 0.0, 600.0, 200.0),
panel_width: 50,
replace_one_glyph: "R1".into(),
replace_all_glyph: "R*".into(),
hit_regions,
}
}
#[test]
fn hit_test_chevron_at_content_origin() {
let p = panel(false);
let hit = p.hit_test(11.0, 12.0, (10.0, 10.0), 8.0, 16.0);
assert_eq!(hit, FindReplaceHit::Target(FindReplaceClickTarget::Chevron));
}
#[test]
fn hit_test_toggle_case_on_find_row() {
let p = panel(false);
let (region, _) = p
.hit_regions
.iter()
.find(|(_, t)| matches!(t, FindReplaceClickTarget::ToggleCase))
.expect("ToggleCase region present");
let x = 10.0 + (region.col as f32 + 0.5) * 8.0;
let y = 10.0 + (region.row as f32 + 0.5) * 16.0;
let hit = p.hit_test(x, y, (10.0, 10.0), 8.0, 16.0);
assert_eq!(
hit,
FindReplaceHit::Target(FindReplaceClickTarget::ToggleCase)
);
}
#[test]
fn hit_test_replace_row_only_present_when_show_replace() {
let p = panel(true);
let (region, _) = p
.hit_regions
.iter()
.find(|(_, t)| matches!(t, FindReplaceClickTarget::ReplaceCurrent))
.expect("ReplaceCurrent region present when show_replace");
let x = 10.0 + (region.col as f32 + 0.5) * 8.0;
let y = 10.0 + (region.row as f32 + 0.5) * 16.0;
assert_eq!(
p.hit_test(x, y, (10.0, 10.0), 8.0, 16.0),
FindReplaceHit::Target(FindReplaceClickTarget::ReplaceCurrent)
);
}
#[test]
fn hit_test_miss_returns_empty() {
let p = panel(false);
assert_eq!(
p.hit_test(0.0, 0.0, (10.0, 10.0), 8.0, 16.0),
FindReplaceHit::Empty
);
}
#[test]
fn hit_test_zero_metrics_returns_empty() {
let p = panel(false);
assert_eq!(
p.hit_test(11.0, 12.0, (10.0, 10.0), 0.0, 16.0),
FindReplaceHit::Empty
);
}
#[test]
fn hit_test_right_edge_is_exclusive() {
let p = panel(false);
let (region, target) = p
.hit_regions
.iter()
.find(|(_, t)| matches!(t, FindReplaceClickTarget::ToggleCase))
.expect("ToggleCase region present");
let target = *target;
let rx1 = 10.0 + (region.col as f32 + region.width as f32) * 8.0;
let y = 10.0 + (region.row as f32 + 0.5) * 16.0;
let hit_at_edge = p.hit_test(rx1, y, (10.0, 10.0), 8.0, 16.0);
assert_ne!(hit_at_edge, FindReplaceHit::Target(target));
let eps = 0.01;
let hit_just_inside = p.hit_test(rx1 - eps, y, (10.0, 10.0), 8.0, 16.0);
assert_eq!(hit_just_inside, FindReplaceHit::Target(target));
}
#[test]
fn hit_test_fractional_char_width_diverges_from_cell_space() {
let p = panel(false);
let (region, target) = p
.hit_regions
.iter()
.find(|(_, t)| matches!(t, FindReplaceClickTarget::ToggleCase))
.expect("ToggleCase region present");
let target = *target;
let cw = 7.5_f32;
let lh = 16.0_f32;
let rx0 = 10.0 + region.col as f32 * cw;
let x = rx0 + 0.5;
let y = 10.0 + (region.row as f32 + 0.5) * lh;
let hit = p.hit_test(x, y, (10.0, 10.0), cw, lh);
assert_eq!(hit, FindReplaceHit::Target(target));
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(unused_imports)]
pub(crate) use native_surface_paint::paint;