Find & Replace
Group: Content
Code
/// Narrower than [`FR_PANEL_WIDTH`] — the "Compact" variant.
const COMPACT_WIDTH: u16 = 36;
pub struct FindReplaceDemo {
panel: FindReplacePanel,
last_click: Option<String>,
}
impl FindReplaceDemo {
pub fn new() -> Self {
Self {
panel: FindReplacePanel {
query: "needle".into(),
replacement: String::new(),
show_replace: true,
focus: 0,
cursor: 6,
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, 0.0, 0.0),
panel_width: FR_PANEL_WIDTH,
replace_one_glyph: "R1".into(),
replace_all_glyph: "R*".into(),
hit_regions: Vec::new(),
},
last_click: None,
}
}
fn panel_width_for(variant: usize) -> u16 {
if variant == 0 {
COMPACT_WIDTH
} else {
FR_PANEL_WIDTH
}
}
/// Build the panel actually painted/hit-tested for `variant`:
/// `self.panel`'s persisted fields (query, toggles, `show_replace`,
/// ...) with `panel_width` and `group_bounds` set for this variant
/// and `hit_regions` recomputed to match — `hit_regions` is laid out
/// *for a specific width*, so reusing the regions computed for one
/// variant while painting another would paint/hit-test columns that
/// don't exist in the narrower box.
fn panel_for(&self, variant: usize, group_bounds: Rect) -> FindReplacePanel {
let panel_width = Self::panel_width_for(variant);
let (hit_regions, _input_w) = compute_find_replace_hit_regions(
panel_width,
self.panel.show_replace,
&self.panel.match_info,
self.panel.replace_one_glyph.chars().count() as u16,
self.panel.replace_all_glyph.chars().count() as u16,
);
let mut panel = self.panel.clone();
panel.panel_width = panel_width;
panel.group_bounds = group_bounds;
panel.hit_regions = hit_regions;
panel
}
/// The `area` `render` paints the panel into, used to keep
/// `panel.group_bounds` and click routing in agreement.
fn editor_rect(area: Rect, backend: &dyn Backend) -> Rect {
let lh = backend.line_height();
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0))
}
/// The panel's content corner (inside its one-cell border), in the
/// same absolute coordinates the rasteriser paints at, scaled by the
/// real `char_width`/`line_height` of the backend that is about to
/// hit-test against it (see the module doc for why this is a
/// cross-backend *approximation*, not each backend's exact anchor
/// formula).
fn content_origin(
panel: &FindReplacePanel,
rect: Rect,
char_width: f32,
line_height: f32,
) -> (f32, f32) {
let cw = char_width.max(1.0);
let lh = line_height.max(1.0);
let panel_w = (panel.panel_width as f32 * cw).min((rect.width - 2.0 * cw).max(0.0));
let gb = &panel.group_bounds;
let gb_right = rect.x + gb.x + gb.width;
let x = (gb_right - (panel_w + cw)).max(rect.x);
let y = gb.y.max(lh);
(x + cw, y + lh)
}
fn status_bar(&self) -> StatusBar {
let msg = match &self.last_click {
Some(m) => format!(" {m} "),
None => " click a toggle or nav button ".into(),
};
StatusBar {
id: WidgetId::new("gallery:find-replace:status"),
left_segments: vec![StatusBarSegment {
text: msg,
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(40, 80, 120),
bold: false,
action_id: None,
}],
right_segments: vec![],
}
}
}
impl Default for FindReplaceDemo {
fn default() -> Self {
Self::new()
}
}
impl Demo for FindReplaceDemo {
fn name(&self) -> &'static str {
"Find & Replace"
}
fn group(&self) -> &'static str {
"Content"
}
fn variants(&self) -> &'static [&'static str] {
&["Compact", "Wide"]
}
fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
let rect = Self::editor_rect(area, backend);
let panel = self.panel_for(variant, Rect::new(0.0, 0.0, rect.width, rect.height));
backend.draw_find_replace(rect, &panel);
let lh = backend.line_height();
let status_rect = Rect::new(area.x, rect.y + rect.height, area.width, lh);
let _ = backend.draw_status_bar_interactive(
status_rect,
&self.status_bar(),
&InteractionState::new(),
);
}
fn handle(
&mut self,
variant: usize,
event: &UiEvent,
backend: &mut dyn Backend,
area: Rect,
) -> Reaction {
match event {
UiEvent::MouseDown {
button: MouseButton::Left,
position,
..
} => {
let rect = Self::editor_rect(area, backend);
let group_bounds = Rect::new(0.0, 0.0, rect.width, rect.height);
let probe = self.panel_for(variant, group_bounds);
let char_width = backend.char_width();
let line_height = backend.line_height();
let origin = Self::content_origin(&probe, rect, char_width, line_height);
let hit = probe.hit_test(position.x, position.y, origin, char_width, line_height);
self.panel.group_bounds = group_bounds;
self.last_click = Some(match hit {
FindReplaceHit::Target(target) => {
match target {
FindReplaceClickTarget::ToggleCase => {
self.panel.case_sensitive = !self.panel.case_sensitive
}
FindReplaceClickTarget::ToggleWholeWord => {
self.panel.whole_word = !self.panel.whole_word
}
FindReplaceClickTarget::ToggleRegex => {
self.panel.use_regex = !self.panel.use_regex
}
FindReplaceClickTarget::TogglePreserveCase => {
self.panel.preserve_case = !self.panel.preserve_case
}
FindReplaceClickTarget::ToggleInSelection => {
self.panel.in_selection = !self.panel.in_selection
}
FindReplaceClickTarget::Chevron => {
self.panel.show_replace = !self.panel.show_replace;
}
_ => {}
}
format!("clicked {target:?}")
}
FindReplaceHit::Empty => "clicked empty area".into(),
});
Reaction::Redraw
}
_ => Reaction::Continue,
}
}
fn source(&self) -> &'static str {
extract_region(SOURCE)
}
fn data(&self, variant: usize) -> serde_json::Value {
// `FindReplacePanel` doesn't derive `Serialize` (it's not meant
// to round-trip, unlike most primitives) — report the fields a
// consumer would actually care about by hand.
serde_json::json!({
"query": self.panel.query,
"replacement": self.panel.replacement,
"show_replace": self.panel.show_replace,
"panel_width": Self::panel_width_for(variant),
"case_sensitive": self.panel.case_sensitive,
"whole_word": self.panel.whole_word,
"use_regex": self.panel.use_regex,
"preserve_case": self.panel.preserve_case,
"in_selection": self.panel.in_selection,
"match_info": self.panel.match_info,
"last_click": self.last_click,
})
}
fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
None
}
}
Backends
| Variant | TUI | GTK | macOS | Windows |
|---|---|---|---|---|
| Compact | ||||
| Wide |