use std::collections::HashMap;
use crate::accelerator::{key_to_binding_name, parse_binding};
use crate::backend::InvalidationLog;
use crate::dispatch::{DragState, TextRegion};
use crate::event::{Key, Point, Rect};
use crate::text_selection::{ActiveTextSelection, TextSelectionState};
use crate::types::{Modifiers, WidgetId};
use crate::{Accelerator, AcceleratorId, AcceleratorScope, ParsedBinding, UiEvent};
#[derive(Default)]
pub(crate) struct BackendCore {
accelerators: HashMap<AcceleratorId, Accelerator>,
parsed_accelerators: Vec<(ParsedBinding, AcceleratorId)>,
pub(crate) text_selection: TextSelectionState,
menu_style: crate::backend::MenuStyle,
invalidation_log: InvalidationLog,
}
impl BackendCore {
#[allow(dead_code)]
pub(crate) fn register_accelerator(&mut self, acc: &Accelerator) {
self.register_accelerator_with(acc, |parsed| parsed);
}
pub(crate) fn register_accelerator_with(
&mut self,
acc: &Accelerator,
transform: impl FnOnce(ParsedBinding) -> ParsedBinding,
) {
self.accelerators.insert(acc.id.clone(), acc.clone());
self.parsed_accelerators.retain(|(_, id)| id != &acc.id);
if let Some(parsed) = parse_binding(&acc.binding) {
self.parsed_accelerators
.push((transform(parsed), acc.id.clone()));
}
}
pub(crate) fn unregister_accelerator(&mut self, id: &AcceleratorId) {
self.accelerators.remove(id);
self.parsed_accelerators.retain(|(_, eid)| eid != id);
}
pub(crate) fn match_keypress(&self, key: &Key, modifiers: Modifiers) -> Option<AcceleratorId> {
let key_name = key_to_binding_name(key);
for (parsed, id) in &self.parsed_accelerators {
if parsed.modifiers == modifiers && parsed.key == key_name {
if let Some(acc) = self.accelerators.get(id) {
if matches!(acc.scope, AcceleratorScope::Global) {
return Some(id.clone());
}
}
}
}
None
}
#[allow(dead_code)]
pub(crate) fn apply_accelerators(&self, events: &mut [UiEvent]) {
if self.parsed_accelerators.is_empty() {
return;
}
for ev in events.iter_mut() {
if let UiEvent::KeyPressed { key, modifiers, .. } = ev {
if let Some(id) = self.match_keypress(key, *modifiers) {
*ev = UiEvent::Accelerator(id, *modifiers);
}
}
}
}
pub(crate) fn menu_style(&self) -> crate::backend::MenuStyle {
self.menu_style
}
pub(crate) fn set_menu_style(&mut self, style: crate::backend::MenuStyle) {
self.menu_style = style;
}
pub(crate) fn record_invalidation(&mut self, rect: Rect) {
self.invalidation_log.push(rect);
}
pub(crate) fn invalidation_log(&self) -> &InvalidationLog {
&self.invalidation_log
}
pub(crate) fn begin_frame(&mut self) {
self.text_selection.begin_frame();
}
pub(crate) fn register_text_region(&mut self, region: TextRegion) {
self.text_selection.register_text_region(region);
}
pub(crate) fn active_text_selection(&self) -> Option<&ActiveTextSelection> {
self.text_selection.active_text_selection()
}
pub(crate) fn set_active_text_selection(
&mut self,
region: WidgetId,
anchor: Point,
focus: Point,
) {
self.text_selection
.set_active_text_selection(region, anchor, focus);
}
pub(crate) fn clear_selection_display(&mut self) {
self.text_selection.clear_selection_display();
}
pub(crate) fn clear_text_selection(&mut self, drag_state: &mut DragState) {
self.text_selection.clear_text_selection(drag_state);
}
pub(crate) fn cancel_text_selection_drag(&mut self, drag_state: &mut DragState) {
self.text_selection.cancel_text_selection_drag(drag_state);
}
pub(crate) fn select_all_text_region(&mut self) -> bool {
self.text_selection.select_all_text_region()
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
pub(crate) fn selection_highlight_rects(
&self,
line_height: f32,
char_width: f32,
) -> Option<Vec<Rect>> {
let sel = self.text_selection.active_text_selection()?;
let region = self.text_selection.find_region(&sel.region)?;
let ranges = crate::text_selection::pixel_selection_ranges(
region.bounds,
sel.anchor,
sel.focus,
line_height,
char_width,
)?;
if ranges.is_empty() {
return None;
}
let rects = crate::paint_geometry::text_selection_highlight_rects(
region.bounds,
&ranges,
char_width as f64,
line_height as f64,
);
if rects.is_empty() {
None
} else {
Some(rects)
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
pub(crate) fn extract_selection_text_pixel(&self, line_height: f32, char_width: f32) -> String {
let Some(sel) = self.text_selection.active_text_selection() else {
return String::new();
};
let Some(region) = self.text_selection.find_region(&sel.region) else {
return String::new();
};
crate::text_selection::extract_lines_pixel(
region,
sel.anchor,
sel.focus,
line_height,
char_width,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accelerator::KeyBinding;
use crate::event::NamedKey;
fn save_accelerator() -> Accelerator {
Accelerator {
id: AcceleratorId::new("app.save"),
binding: KeyBinding::Save,
scope: AcceleratorScope::Global,
label: None,
}
}
#[test]
fn register_then_match_finds_the_binding() {
let mut core = BackendCore::default();
core.register_accelerator(&save_accelerator());
let id = core.match_keypress(
&Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
},
);
assert_eq!(id, Some(AcceleratorId::new("app.save")));
}
#[test]
fn unregister_removes_the_match() {
let mut core = BackendCore::default();
core.register_accelerator(&save_accelerator());
core.unregister_accelerator(&AcceleratorId::new("app.save"));
assert_eq!(
core.match_keypress(
&Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
}
),
None
);
}
#[test]
fn register_accelerator_with_applies_the_transform() {
let mut core = BackendCore::default();
core.register_accelerator_with(&save_accelerator(), |mut parsed| {
parsed.modifiers.cmd = true;
parsed.modifiers.ctrl = false;
parsed
});
assert_eq!(
core.match_keypress(
&Key::Char('s'),
Modifiers {
cmd: true,
..Default::default()
}
),
Some(AcceleratorId::new("app.save"))
);
assert_eq!(
core.match_keypress(
&Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
}
),
None
);
}
#[test]
fn apply_accelerators_rewrites_matching_key_presses() {
let mut core = BackendCore::default();
core.register_accelerator(&save_accelerator());
let mut events = vec![UiEvent::KeyPressed {
key: Key::Char('s'),
modifiers: Modifiers {
ctrl: true,
..Default::default()
},
repeat: false,
}];
core.apply_accelerators(&mut events);
assert_eq!(
events[0],
UiEvent::Accelerator(
AcceleratorId::new("app.save"),
Modifiers {
ctrl: true,
..Default::default()
}
)
);
}
#[test]
fn apply_accelerators_ignores_non_global_scope() {
let mut core = BackendCore::default();
core.register_accelerator(&Accelerator {
id: AcceleratorId::new("widget.esc"),
binding: KeyBinding::Literal("<Escape>".into()),
scope: AcceleratorScope::Widget(WidgetId::new("picker")),
label: None,
});
let mut events = vec![UiEvent::KeyPressed {
key: Key::Named(NamedKey::Escape),
modifiers: Modifiers::default(),
repeat: false,
}];
core.apply_accelerators(&mut events);
assert!(matches!(events[0], UiEvent::KeyPressed { .. }));
}
#[test]
fn text_selection_round_trip_through_the_registry() {
let mut core = BackendCore::default();
core.register_text_region(TextRegion {
id: WidgetId::new("body"),
bounds: crate::event::Rect::new(0.0, 0.0, 100.0, 50.0),
lines: vec![],
});
assert!(core.select_all_text_region());
assert!(core.active_text_selection().is_some());
core.clear_selection_display();
assert!(core.active_text_selection().is_none());
core.begin_frame();
assert!(core.text_selection.text_regions.is_empty());
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn selection_highlight_rects_none_with_no_active_selection() {
let core = BackendCore::default();
assert!(core.selection_highlight_rects(20.0, 10.0).is_none());
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn selection_highlight_rects_resolves_a_single_row() {
let mut core = BackendCore::default();
core.register_text_region(TextRegion {
id: WidgetId::new("body"),
bounds: crate::event::Rect::new(0.0, 0.0, 200.0, 100.0),
lines: vec!["hello world".into()],
});
core.set_active_text_selection(
WidgetId::new("body"),
Point::new(0.0, 0.0),
Point::new(50.0, 5.0),
);
let rects = core
.selection_highlight_rects(20.0, 10.0)
.expect("a single-row selection resolves to at least one rect");
assert_eq!(rects.len(), 1);
assert_eq!(rects[0], Rect::new(0.0, 0.0, 60.0, 20.0));
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn extract_selection_text_pixel_reads_the_region_lines() {
let mut core = BackendCore::default();
core.register_text_region(TextRegion {
id: WidgetId::new("body"),
bounds: crate::event::Rect::new(0.0, 0.0, 200.0, 100.0),
lines: vec!["hello world".into()],
});
core.set_active_text_selection(
WidgetId::new("body"),
Point::new(0.0, 0.0),
Point::new(50.0, 5.0),
);
assert_eq!(core.extract_selection_text_pixel(20.0, 10.0), "hello");
}
#[test]
fn record_invalidation_appends_to_the_log() {
let mut core = BackendCore::default();
assert!(core.invalidation_log().is_empty());
let rect = crate::event::Rect::new(1.0, 2.0, 3.0, 4.0);
core.record_invalidation(rect);
assert_eq!(core.invalidation_log().len(), 1);
assert_eq!(core.invalidation_log().rects(), &[rect]);
}
}