Mouse input improvements
This commit is contained in:
@@ -2688,8 +2688,7 @@ mod tests {
|
|||||||
drill.started_at = Some(now);
|
drill.started_at = Some(now);
|
||||||
drill.finished_at = Some(now + Duration::from_millis(200 * (target.len() as u64)));
|
drill.finished_at = Some(now + Duration::from_millis(200 * (target.len() as u64)));
|
||||||
drill.cursor = drill.target.len();
|
drill.cursor = drill.target.len();
|
||||||
drill.input =
|
drill.input = vec![crate::session::input::CharStatus::Correct; drill.target.len()];
|
||||||
vec![crate::session::input::CharStatus::Correct; drill.target.len()];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1177,18 +1177,32 @@ mod tests {
|
|||||||
found_available = true;
|
found_available = true;
|
||||||
// Should contain exactly the 5 non-lowercase branches
|
// Should contain exactly the 5 non-lowercase branches
|
||||||
assert_eq!(result.branches_newly_available.len(), 5);
|
assert_eq!(result.branches_newly_available.len(), 5);
|
||||||
assert!(!result.branches_newly_available.contains(&BranchId::Lowercase));
|
assert!(
|
||||||
assert!(result.branches_newly_available.contains(&BranchId::Capitals));
|
!result
|
||||||
|
.branches_newly_available
|
||||||
|
.contains(&BranchId::Lowercase)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
|
.branches_newly_available
|
||||||
|
.contains(&BranchId::Capitals)
|
||||||
|
);
|
||||||
assert!(result.branches_newly_available.contains(&BranchId::Numbers));
|
assert!(result.branches_newly_available.contains(&BranchId::Numbers));
|
||||||
assert!(result
|
assert!(
|
||||||
|
result
|
||||||
.branches_newly_available
|
.branches_newly_available
|
||||||
.contains(&BranchId::ProsePunctuation));
|
.contains(&BranchId::ProsePunctuation)
|
||||||
assert!(result
|
);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
.branches_newly_available
|
.branches_newly_available
|
||||||
.contains(&BranchId::Whitespace));
|
.contains(&BranchId::Whitespace)
|
||||||
assert!(result
|
);
|
||||||
|
assert!(
|
||||||
|
result
|
||||||
.branches_newly_available
|
.branches_newly_available
|
||||||
.contains(&BranchId::CodeSymbols));
|
.contains(&BranchId::CodeSymbols)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1218,7 +1232,10 @@ mod tests {
|
|||||||
let mut found_complete = false;
|
let mut found_complete = false;
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
let result = tree.update(&stats, None);
|
let result = tree.update(&stats, None);
|
||||||
if result.branches_newly_completed.contains(&BranchId::Capitals) {
|
if result
|
||||||
|
.branches_newly_completed
|
||||||
|
.contains(&BranchId::Capitals)
|
||||||
|
{
|
||||||
found_complete = true;
|
found_complete = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1231,7 +1248,9 @@ mod tests {
|
|||||||
// Second update should not re-report
|
// Second update should not re-report
|
||||||
let result2 = tree.update(&stats, None);
|
let result2 = tree.update(&stats, None);
|
||||||
assert!(
|
assert!(
|
||||||
!result2.branches_newly_completed.contains(&BranchId::Capitals),
|
!result2
|
||||||
|
.branches_newly_completed
|
||||||
|
.contains(&BranchId::Capitals),
|
||||||
"should not re-report Capitals as newly completed"
|
"should not re-report Capitals as newly completed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/main.rs
108
src/main.rs
@@ -39,8 +39,8 @@ use keyboard::finger::Hand;
|
|||||||
use ui::components::dashboard::Dashboard;
|
use ui::components::dashboard::Dashboard;
|
||||||
use ui::components::keyboard_diagram::KeyboardDiagram;
|
use ui::components::keyboard_diagram::KeyboardDiagram;
|
||||||
use ui::components::skill_tree::{
|
use ui::components::skill_tree::{
|
||||||
SkillTreeWidget, detail_line_count_with_level_spacing, selectable_branches,
|
SkillTreeWidget, branch_list_spacing_flags, detail_line_count_with_level_spacing,
|
||||||
use_expanded_level_spacing, use_side_by_side_layout,
|
selectable_branches, use_expanded_level_spacing, use_side_by_side_layout,
|
||||||
};
|
};
|
||||||
use ui::components::stats_dashboard::{
|
use ui::components::stats_dashboard::{
|
||||||
AnomalyBigramRow, NgramTabData, StatsDashboard, history_page_size_for_terminal,
|
AnomalyBigramRow, NgramTabData, StatsDashboard, history_page_size_for_terminal,
|
||||||
@@ -1844,7 +1844,14 @@ fn handle_skill_tree_key(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skill_tree_interactive_areas(app: &App, area: Rect) -> (Rect, Rect) {
|
struct SkillTreeMouseLayout {
|
||||||
|
branch_area: Rect,
|
||||||
|
detail_area: Rect,
|
||||||
|
inter_branch_spacing: bool,
|
||||||
|
separator_padding: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skill_tree_interactive_areas(app: &App, area: Rect) -> SkillTreeMouseLayout {
|
||||||
let centered = skill_tree_popup_rect(area);
|
let centered = skill_tree_popup_rect(area);
|
||||||
let inner = Block::bordered().inner(centered);
|
let inner = Block::bordered().inner(centered);
|
||||||
let branches = selectable_branches();
|
let branches = selectable_branches();
|
||||||
@@ -1914,7 +1921,14 @@ fn skill_tree_interactive_areas(app: &App, area: Rect) -> (Rect, Rect) {
|
|||||||
Constraint::Percentage(58),
|
Constraint::Percentage(58),
|
||||||
])
|
])
|
||||||
.split(layout[0]);
|
.split(layout[0]);
|
||||||
(main[0], main[2])
|
let (inter_branch_spacing, separator_padding) =
|
||||||
|
branch_list_spacing_flags(main[0].height, branches.len());
|
||||||
|
SkillTreeMouseLayout {
|
||||||
|
branch_area: main[0],
|
||||||
|
detail_area: main[2],
|
||||||
|
inter_branch_spacing,
|
||||||
|
separator_padding,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let branch_list_height = branches.len() as u16 * 2 + 1;
|
let branch_list_height = branches.len() as u16 * 2 + 1;
|
||||||
let main = Layout::default()
|
let main = Layout::default()
|
||||||
@@ -1925,9 +1939,50 @@ fn skill_tree_interactive_areas(app: &App, area: Rect) -> (Rect, Rect) {
|
|||||||
Constraint::Min(3),
|
Constraint::Min(3),
|
||||||
])
|
])
|
||||||
.split(layout[0]);
|
.split(layout[0]);
|
||||||
(main[0], main[2])
|
SkillTreeMouseLayout {
|
||||||
|
branch_area: main[0],
|
||||||
|
detail_area: main[2],
|
||||||
|
inter_branch_spacing: false,
|
||||||
|
separator_padding: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skill_tree_branch_index_from_y(
|
||||||
|
branch_area: Rect,
|
||||||
|
y: u16,
|
||||||
|
branch_count: usize,
|
||||||
|
inter_branch_spacing: bool,
|
||||||
|
separator_padding: bool,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if y < branch_area.y || y >= branch_area.y + branch_area.height {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rel_y = (y - branch_area.y) as usize;
|
||||||
|
let mut line = 0usize;
|
||||||
|
for idx in 0..branch_count {
|
||||||
|
if idx > 0 && inter_branch_spacing {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
let title_line = line;
|
||||||
|
let progress_line = line + 1;
|
||||||
|
if rel_y == title_line || rel_y == progress_line {
|
||||||
|
return Some(idx);
|
||||||
|
}
|
||||||
|
line += 2;
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
if separator_padding {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
line += 1;
|
||||||
|
if separator_padding && !inter_branch_spacing {
|
||||||
|
line += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_skill_tree_mouse(app: &mut App, mouse: MouseEvent) {
|
fn handle_skill_tree_mouse(app: &mut App, mouse: MouseEvent) {
|
||||||
const DETAIL_SCROLL_STEP: usize = 3;
|
const DETAIL_SCROLL_STEP: usize = 3;
|
||||||
@@ -1975,10 +2030,15 @@ fn handle_skill_tree_mouse(app: &mut App, mouse: MouseEvent) {
|
|||||||
}
|
}
|
||||||
MouseEventKind::Down(MouseButton::Left) => {
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
let branches = selectable_branches();
|
let branches = selectable_branches();
|
||||||
let (branch_area, detail_area) = skill_tree_interactive_areas(app, terminal_area());
|
let layout = skill_tree_interactive_areas(app, terminal_area());
|
||||||
if point_in_rect(mouse.column, mouse.row, branch_area) {
|
if point_in_rect(mouse.column, mouse.row, layout.branch_area) {
|
||||||
let relative = (mouse.row - branch_area.y) as usize;
|
if let Some(idx) = skill_tree_branch_index_from_y(
|
||||||
let idx = (relative / 2).min(branches.len().saturating_sub(1));
|
layout.branch_area,
|
||||||
|
mouse.row,
|
||||||
|
branches.len(),
|
||||||
|
layout.inter_branch_spacing,
|
||||||
|
layout.separator_padding,
|
||||||
|
) {
|
||||||
let already_selected = idx == app.skill_tree_selected;
|
let already_selected = idx == app.skill_tree_selected;
|
||||||
app.skill_tree_selected = idx;
|
app.skill_tree_selected = idx;
|
||||||
app.skill_tree_detail_scroll = 0;
|
app.skill_tree_detail_scroll = 0;
|
||||||
@@ -1991,9 +2051,10 @@ fn handle_skill_tree_mouse(app: &mut App, mouse: MouseEvent) {
|
|||||||
app.start_branch_drill(branch_id);
|
app.start_branch_drill(branch_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if point_in_rect(mouse.column, mouse.row, detail_area) {
|
}
|
||||||
|
} else if point_in_rect(mouse.column, mouse.row, layout.detail_area) {
|
||||||
// Click in detail pane focuses selected branch; scroll wheel handles movement.
|
// Click in detail pane focuses selected branch; scroll wheel handles movement.
|
||||||
let _ = detail_area;
|
let _ = layout.detail_area;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -2439,7 +2500,10 @@ fn render_milestone_overlay(
|
|||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let colors = &app.theme.colors;
|
let colors = &app.theme.colors;
|
||||||
|
|
||||||
let is_key_milestone = matches!(milestone.kind, MilestoneKind::Unlock | MilestoneKind::Mastery);
|
let is_key_milestone = matches!(
|
||||||
|
milestone.kind,
|
||||||
|
MilestoneKind::Unlock | MilestoneKind::Mastery
|
||||||
|
);
|
||||||
|
|
||||||
// Determine overlay size based on terminal height:
|
// Determine overlay size based on terminal height:
|
||||||
// Key milestones get keyboard diagrams; other milestones are text-only
|
// Key milestones get keyboard diagrams; other milestones are text-only
|
||||||
@@ -2610,10 +2674,7 @@ fn render_milestone_overlay(
|
|||||||
.map(|&id| get_branch_definition(id).name)
|
.map(|&id| get_branch_definition(id).name)
|
||||||
.collect();
|
.collect();
|
||||||
let branches_text = if branch_names.len() == 1 {
|
let branches_text = if branch_names.len() == 1 {
|
||||||
format!(
|
format!(" You've fully mastered the {} branch!", branch_names[0])
|
||||||
" You've fully mastered the {} branch!",
|
|
||||||
branch_names[0]
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
let all_but_last = &branch_names[..branch_names.len() - 1];
|
let all_but_last = &branch_names[..branch_names.len() - 1];
|
||||||
let last = branch_names[branch_names.len() - 1];
|
let last = branch_names[branch_names.len() - 1];
|
||||||
@@ -5211,6 +5272,21 @@ fn handle_keyboard_explorer_mouse(app: &mut App, mouse: MouseEvent) {
|
|||||||
.split(area);
|
.split(area);
|
||||||
if point_in_rect(mouse.column, mouse.row, layout[3]) {
|
if point_in_rect(mouse.column, mouse.row, layout[3]) {
|
||||||
app.go_to_menu();
|
app.go_to_menu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if point_in_rect(mouse.column, mouse.row, layout[1])
|
||||||
|
&& let Some(ch) = KeyboardDiagram::key_at_position(
|
||||||
|
layout[1],
|
||||||
|
&app.keyboard_model,
|
||||||
|
false,
|
||||||
|
mouse.column,
|
||||||
|
mouse.row,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
app.keyboard_explorer_selected = Some(ch);
|
||||||
|
app.key_accuracy(ch, false);
|
||||||
|
app.key_accuracy(ch, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,25 @@ impl Widget for KeyboardDiagram<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl KeyboardDiagram<'_> {
|
impl KeyboardDiagram<'_> {
|
||||||
|
pub fn key_at_position(
|
||||||
|
area: Rect,
|
||||||
|
model: &KeyboardModel,
|
||||||
|
compact: bool,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
) -> Option<char> {
|
||||||
|
let inner = Block::bordered().inner(area);
|
||||||
|
if compact {
|
||||||
|
return key_at_compact_position(inner, model, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner.height >= 4 && inner.width >= 75 {
|
||||||
|
key_at_full_position(inner, model, x, y)
|
||||||
|
} else {
|
||||||
|
key_at_full_fallback_position(inner, model, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_compact(&self, inner: Rect, buf: &mut Buffer) {
|
fn render_compact(&self, inner: Rect, buf: &mut Buffer) {
|
||||||
let colors = &self.theme.colors;
|
let colors = &self.theme.colors;
|
||||||
let letter_rows = self.model.letter_rows();
|
let letter_rows = self.model.letter_rows();
|
||||||
@@ -597,3 +616,230 @@ impl KeyboardDiagram<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rect_contains(area: Rect, x: u16, y: u16) -> bool {
|
||||||
|
x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_at_compact_position(inner: Rect, model: &KeyboardModel, x: u16, y: u16) -> Option<char> {
|
||||||
|
let letter_rows = model.letter_rows();
|
||||||
|
let key_width: u16 = 3;
|
||||||
|
let min_width: u16 = 21;
|
||||||
|
if inner.height < 3 || inner.width < min_width {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offsets: &[u16] = &[3, 4, 6];
|
||||||
|
let keyboard_width = letter_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(row_idx, row)| {
|
||||||
|
let offset = offsets.get(row_idx).copied().unwrap_or(0);
|
||||||
|
let row_end = offset + row.len() as u16 * key_width;
|
||||||
|
match row_idx {
|
||||||
|
0 => row_end + 3,
|
||||||
|
1 => row_end + 3,
|
||||||
|
2 => row_end + 3,
|
||||||
|
_ => row_end,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let start_x = inner.x + inner.width.saturating_sub(keyboard_width) / 2;
|
||||||
|
|
||||||
|
for (row_idx, row) in letter_rows.iter().enumerate() {
|
||||||
|
let row_y = inner.y + row_idx as u16;
|
||||||
|
if y != row_y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match row_idx {
|
||||||
|
0 => {
|
||||||
|
let tab_rect = Rect::new(start_x, row_y, 3, 1);
|
||||||
|
if rect_contains(tab_rect, x, y) {
|
||||||
|
return Some(TAB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let shft_rect = Rect::new(start_x, row_y, 3, 1);
|
||||||
|
if rect_contains(shft_rect, x, y) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = offsets.get(row_idx).copied().unwrap_or(0);
|
||||||
|
for (col_idx, key) in row.iter().enumerate() {
|
||||||
|
let key_x = start_x + offset + col_idx as u16 * key_width;
|
||||||
|
let key_rect = Rect::new(key_x, row_y, 3, 1);
|
||||||
|
if rect_contains(key_rect, x, y) {
|
||||||
|
return Some(key.base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let row_end_x = start_x + offset + row.len() as u16 * key_width;
|
||||||
|
match row_idx {
|
||||||
|
1 => {
|
||||||
|
let enter_rect = Rect::new(row_end_x, row_y, 3, 1);
|
||||||
|
if rect_contains(enter_rect, x, y) {
|
||||||
|
return Some(ENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let shft_rect = Rect::new(row_end_x, row_y, 3, 1);
|
||||||
|
if rect_contains(shft_rect, x, y) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner.height >= 3 {
|
||||||
|
let y0 = inner.y;
|
||||||
|
let row_end_x = start_x + offsets[0] + letter_rows[0].len() as u16 * key_width;
|
||||||
|
let back_rect = Rect::new(row_end_x, y0, 3, 1);
|
||||||
|
if rect_contains(back_rect, x, y) {
|
||||||
|
return Some(BACKSPACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_at_full_position(inner: Rect, model: &KeyboardModel, x: u16, y: u16) -> Option<char> {
|
||||||
|
let key_width: u16 = 5;
|
||||||
|
let offsets: &[u16] = &[0, 5, 5, 6];
|
||||||
|
let keyboard_width = model
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(row_idx, row)| {
|
||||||
|
let offset = offsets.get(row_idx).copied().unwrap_or(0);
|
||||||
|
let row_end = offset + row.len() as u16 * key_width;
|
||||||
|
match row_idx {
|
||||||
|
0 => row_end + 6,
|
||||||
|
2 => row_end + 7,
|
||||||
|
3 => row_end + 6,
|
||||||
|
_ => row_end,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let start_x = inner.x + inner.width.saturating_sub(keyboard_width) / 2;
|
||||||
|
|
||||||
|
for (row_idx, row) in model.rows.iter().enumerate() {
|
||||||
|
let row_y = inner.y + row_idx as u16;
|
||||||
|
if y != row_y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let offset = offsets.get(row_idx).copied().unwrap_or(0);
|
||||||
|
|
||||||
|
match row_idx {
|
||||||
|
1 => {
|
||||||
|
let tab_rect = Rect::new(start_x, row_y, 5, 1);
|
||||||
|
if rect_contains(tab_rect, x, y) {
|
||||||
|
return Some(TAB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let cap_rect = Rect::new(start_x, row_y, 5, 1);
|
||||||
|
if rect_contains(cap_rect, x, y) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let shft_rect = Rect::new(start_x, row_y, 6, 1);
|
||||||
|
if rect_contains(shft_rect, x, y) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (col_idx, key) in row.iter().enumerate() {
|
||||||
|
let key_x = start_x + offset + col_idx as u16 * key_width;
|
||||||
|
let key_rect = Rect::new(key_x, row_y, key_width, 1);
|
||||||
|
if rect_contains(key_rect, x, y) {
|
||||||
|
return Some(key.base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let after_x = start_x + offset + row.len() as u16 * key_width;
|
||||||
|
match row_idx {
|
||||||
|
0 => {
|
||||||
|
let rect = Rect::new(after_x, row_y, 6, 1);
|
||||||
|
if rect_contains(rect, x, y) {
|
||||||
|
return Some(BACKSPACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let rect = Rect::new(after_x, row_y, 7, 1);
|
||||||
|
if rect_contains(rect, x, y) {
|
||||||
|
return Some(ENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let rect = Rect::new(after_x, row_y, 6, 1);
|
||||||
|
if rect_contains(rect, x, y) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let space_y = inner.y + 4;
|
||||||
|
if y == space_y {
|
||||||
|
let space_name = display::key_display_name(SPACE);
|
||||||
|
let space_label = format!("[ {space_name} ]");
|
||||||
|
let space_width = space_label.len() as u16;
|
||||||
|
let space_x = start_x + (keyboard_width.saturating_sub(space_width)) / 2;
|
||||||
|
let space_rect = Rect::new(space_x, space_y, space_width, 1);
|
||||||
|
if rect_contains(space_rect, x, y) {
|
||||||
|
return Some(SPACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_at_full_fallback_position(
|
||||||
|
inner: Rect,
|
||||||
|
model: &KeyboardModel,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
) -> Option<char> {
|
||||||
|
let letter_rows = model.letter_rows();
|
||||||
|
let key_width: u16 = 5;
|
||||||
|
let offsets: &[u16] = &[1, 3, 5];
|
||||||
|
let keyboard_width = letter_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(row_idx, row)| {
|
||||||
|
let offset = offsets.get(row_idx).copied().unwrap_or(0);
|
||||||
|
offset + row.len() as u16 * key_width
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let start_x = inner.x + inner.width.saturating_sub(keyboard_width) / 2;
|
||||||
|
|
||||||
|
if inner.height < 3 || inner.width < 30 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (row_idx, row) in letter_rows.iter().enumerate() {
|
||||||
|
let row_y = inner.y + row_idx as u16;
|
||||||
|
if y != row_y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let offset = offsets.get(row_idx).copied().unwrap_or(0);
|
||||||
|
for (col_idx, key) in row.iter().enumerate() {
|
||||||
|
let key_x = start_x + offset + col_idx as u16 * key_width;
|
||||||
|
let key_rect = Rect::new(key_x, row_y, key_width, 1);
|
||||||
|
if rect_contains(key_rect, x, y) {
|
||||||
|
return Some(key.base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user