use super::accessibility::ClickableElement;
use super::config::ClickHelperConfig;
use super::hints::{HintGenerator, HintLabel};
use egui::{Color32, FontId, Pos2, Rect, Vec2};
pub struct ClickHelperOverlay {
overlay_color: Color32,
pub hints: Vec<(ClickableElement, HintLabel)>,
input_buffer: String,
visible_indices: Vec<usize>,
hint_font_size: f32,
hint_bg_color: Color32,
hint_fg_color: Color32,
}
impl ClickHelperOverlay {
pub fn new(elements: Vec<ClickableElement>, config: &ClickHelperConfig) -> Self {
let hint_gen = HintGenerator::new(config);
let labels = hint_gen.generate(elements.len());
let hints: Vec<_> = elements.into_iter().zip(labels).collect();
let visible_indices: Vec<_> = (0..hints.len()).collect();
Self {
overlay_color: Color32::from_rgba_unmultiplied(0, 0, 0, config.overlay_opacity),
hints,
input_buffer: String::new(),
visible_indices,
hint_font_size: config.hint_font_size,
hint_bg_color: Color32::from_rgba_unmultiplied(
config.hint_bg_color[0],
config.hint_bg_color[1],
config.hint_bg_color[2],
config.hint_bg_color[3],
),
hint_fg_color: Color32::from_rgba_unmultiplied(
config.hint_fg_color[0],
config.hint_fg_color[1],
config.hint_fg_color[2],
config.hint_fg_color[3],
),
}
}
pub fn input_buffer(&self) -> &str {
&self.input_buffer
}
pub fn add_input(&mut self, c: char) {
self.input_buffer.push(c);
self.filter_hints();
}
pub fn backspace(&mut self) {
self.input_buffer.pop();
self.filter_hints();
}
pub fn reset_input(&mut self) {
self.input_buffer.clear();
self.visible_indices = (0..self.hints.len()).collect();
}
fn filter_hints(&mut self) {
if self.input_buffer.is_empty() {
self.visible_indices = (0..self.hints.len()).collect();
} else {
self.visible_indices = self
.hints
.iter()
.enumerate()
.filter(|(_, (_, hint))| hint.label.starts_with(&self.input_buffer))
.map(|(i, _)| i)
.collect();
}
}
pub fn matching_count(&self) -> usize {
self.visible_indices.len()
}
pub fn get_exact_match(&self) -> Option<&ClickableElement> {
if self.visible_indices.len() == 1 {
let idx = self.visible_indices[0];
let (element, hint) = &self.hints[idx];
if hint.label == self.input_buffer {
return Some(element);
}
}
let matching: Vec<_> =
self.hints.iter().filter(|(_, hint)| hint.label == self.input_buffer).collect();
if matching.len() == 1 {
return Some(&matching[0].0);
}
None
}
pub fn get_unique_match(&self) -> Option<&ClickableElement> {
if self.visible_indices.len() == 1 {
let idx = self.visible_indices[0];
return Some(&self.hints[idx].0);
}
None
}
#[allow(deprecated)]
pub fn render(&self, ctx: &egui::Context) {
let screen_rect = ctx.screen_rect();
egui::Area::new(egui::Id::new("click_helper_overlay"))
.fixed_pos(Pos2::ZERO)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
ui.painter().rect_filled(screen_rect, 0.0, self.overlay_color);
for &idx in &self.visible_indices {
let (element, hint) = &self.hints[idx];
self.render_hint_label(ui, element, hint);
}
if !self.input_buffer.is_empty() {
self.render_input_display(ui, screen_rect);
}
});
}
fn render_hint_label(&self, ui: &egui::Ui, element: &ClickableElement, hint: &HintLabel) {
let pos = Pos2::new(element.bounds.0, element.bounds.1);
let display_text = if self.input_buffer.is_empty() {
hint.label.clone()
} else {
hint.label.clone()
};
let font_id = FontId::monospace(self.hint_font_size);
let galley =
ui.painter().layout_no_wrap(display_text.clone(), font_id.clone(), self.hint_fg_color);
let padding = Vec2::new(6.0, 3.0);
let label_size = galley.size() + padding * 2.0;
let label_rect = Rect::from_min_size(pos, label_size);
ui.painter().rect_filled(label_rect, 4.0, self.hint_bg_color);
ui.painter().rect_stroke(
label_rect,
4.0,
egui::Stroke::new(1.0, Color32::from_rgba_unmultiplied(0, 0, 0, 100)),
egui::StrokeKind::Outside,
);
let text_pos = label_rect.min + padding;
if !self.input_buffer.is_empty() && hint.label.starts_with(&self.input_buffer) {
let matched_part = &hint.label[..self.input_buffer.len()];
let remaining_part = &hint.label[self.input_buffer.len()..];
let matched_galley = ui.painter().layout_no_wrap(
matched_part.to_string(),
font_id.clone(),
Color32::from_rgba_unmultiplied(100, 100, 100, 255),
);
ui.painter().galley(text_pos, matched_galley.clone(), Color32::PLACEHOLDER);
if !remaining_part.is_empty() {
let offset = matched_galley.size().x;
let remaining_galley = ui.painter().layout_no_wrap(
remaining_part.to_string(),
font_id,
self.hint_fg_color,
);
ui.painter().galley(
text_pos + Vec2::new(offset, 0.0),
remaining_galley,
Color32::PLACEHOLDER,
);
}
} else {
ui.painter().galley(text_pos, galley, Color32::PLACEHOLDER);
}
}
fn render_input_display(&self, ui: &egui::Ui, screen_rect: Rect) {
let display_text = format!("Input: {}", self.input_buffer);
let font_id = FontId::proportional(18.0);
let galley = ui.painter().layout_no_wrap(display_text, font_id, Color32::WHITE);
let padding = Vec2::new(16.0, 8.0);
let label_size = galley.size() + padding * 2.0;
let pos = Pos2::new((screen_rect.width() - label_size.x) / 2.0, 20.0);
let label_rect = Rect::from_min_size(pos, label_size);
ui.painter().rect_filled(label_rect, 8.0, Color32::from_rgba_unmultiplied(50, 50, 50, 220));
ui.painter().galley(label_rect.min + padding, galley, Color32::PLACEHOLDER);
}
}