use crate::draw_ctx::DrawCtx;
use crate::geometry::{Point, Rect};
use super::key::{KeyAction, KeyCap, KeyGlyph, PaintedKey};
use super::style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Layer {
Letters,
Shifted,
Numbers,
Symbols,
}
#[derive(Debug, Clone)]
struct KeySpec {
width_units: f64,
cap: KeyCap,
action: KeyAction,
kind: KeyKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KeyKind {
Letter,
Utility,
Return,
}
pub struct Layout {
rows: Vec<Vec<KeySpec>>,
}
impl Layout {
pub fn for_layer(layer: Layer) -> Self {
match layer {
Layer::Letters => letters_layer(false),
Layer::Shifted => letters_layer(true),
Layer::Numbers => numbers_layer(),
Layer::Symbols => symbols_layer(),
}
}
pub fn compute_panel_height(&self, _viewport_width: f64, style: &Style) -> f64 {
let rows = self.rows.len() as f64;
style.panel_padding_top
+ style.panel_padding_bottom
+ rows * style.row_height
+ (rows - 1.0).max(0.0) * style.key_v_gap
}
pub fn paint(
&self,
ctx: &mut dyn DrawCtx,
panel: Rect,
style: &Style,
active_layer: Layer,
) -> Vec<PaintedKey> {
let mut painted = Vec::with_capacity(self.rows.iter().map(|r| r.len()).sum());
let inner_x = panel.x + style.panel_padding_horizontal;
let inner_w = panel.width - 2.0 * style.panel_padding_horizontal;
let mut row_top_y = panel.y + panel.height - style.panel_padding_top;
for (row_index, row) in self.rows.iter().enumerate() {
let row_bottom_y = row_top_y - style.row_height;
let total_units: f64 = row.iter().map(|k| k.width_units).sum();
let total_gaps = (row.len() as f64 - 1.0).max(0.0) * style.key_h_gap;
let key_unit_width = (inner_w - total_gaps) / total_units.max(0.001);
let mut cursor_x = inner_x;
for spec in row.iter() {
let kw = spec.width_units * key_unit_width;
let rect = Rect::new(cursor_x, row_bottom_y, kw, style.row_height);
let pressed = false; paint_key(ctx, rect, spec, pressed, style, active_layer);
painted.push(PaintedKey {
rect,
action: spec.action,
cap: spec.cap.clone(),
});
cursor_x += kw + style.key_h_gap;
}
if row_index + 1 < self.rows.len() {
row_top_y = row_bottom_y - style.key_v_gap;
}
}
painted
}
}
fn letters_layer(shifted: bool) -> Layout {
let case = |lower: char, upper: char| if shifted { upper } else { lower };
let row_keys = |letters: &[(char, char)]| -> Vec<KeySpec> {
letters
.iter()
.map(|(lo, up)| {
let c = case(*lo, *up);
KeySpec {
width_units: 1.0,
cap: KeyCap::Text(c.to_string()),
action: KeyAction::Char(c),
kind: KeyKind::Letter,
}
})
.collect()
};
let mut rows: Vec<Vec<KeySpec>> = Vec::with_capacity(4);
rows.push(row_keys(&[
('q', 'Q'),
('w', 'W'),
('e', 'E'),
('r', 'R'),
('t', 'T'),
('y', 'Y'),
('u', 'U'),
('i', 'I'),
('o', 'O'),
('p', 'P'),
]));
let row2 = row_keys(&[
('a', 'A'),
('s', 'S'),
('d', 'D'),
('f', 'F'),
('g', 'G'),
('h', 'H'),
('j', 'J'),
('k', 'K'),
('l', 'L'),
]);
rows.push(row2);
let mut row3: Vec<KeySpec> = Vec::with_capacity(11);
row3.push(KeySpec {
width_units: 1.5,
cap: KeyCap::Glyph(KeyGlyph::Shift),
action: KeyAction::Switch(if shifted {
Layer::Letters
} else {
Layer::Shifted
}),
kind: KeyKind::Utility,
});
row3.extend(row_keys(&[
('z', 'Z'),
('x', 'X'),
('c', 'C'),
('v', 'V'),
('b', 'B'),
('n', 'N'),
('m', 'M'),
]));
row3.push(KeySpec {
width_units: 1.5,
cap: KeyCap::Glyph(KeyGlyph::Backspace),
action: KeyAction::Backspace,
kind: KeyKind::Utility,
});
rows.push(row3);
rows.push(action_row(if shifted {
Layer::Shifted
} else {
Layer::Letters
}));
Layout { rows }
}
fn numbers_layer() -> Layout {
let digit = |c: char| KeySpec {
width_units: 1.0,
cap: KeyCap::Text(c.to_string()),
action: KeyAction::Char(c),
kind: KeyKind::Letter,
};
let mut rows = Vec::with_capacity(4);
rows.push(
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']
.iter()
.map(|c| digit(*c))
.collect(),
);
rows.push(
['-', '/', ':', ';', '(', ')', '$', '&', '@', '"']
.iter()
.map(|c| digit(*c))
.collect(),
);
let mut row3 = Vec::with_capacity(9);
row3.push(KeySpec {
width_units: 1.5,
cap: KeyCap::Text("#+=".to_string()),
action: KeyAction::Switch(Layer::Symbols),
kind: KeyKind::Utility,
});
for c in ['.', ',', '?', '!', '\''] {
row3.push(digit(c));
}
row3.push(KeySpec {
width_units: 1.5,
cap: KeyCap::Glyph(KeyGlyph::Backspace),
action: KeyAction::Backspace,
kind: KeyKind::Utility,
});
rows.push(row3);
rows.push(action_row(Layer::Numbers));
Layout { rows }
}
fn symbols_layer() -> Layout {
let sym = |c: char| KeySpec {
width_units: 1.0,
cap: KeyCap::Text(c.to_string()),
action: KeyAction::Char(c),
kind: KeyKind::Letter,
};
let mut rows = Vec::with_capacity(4);
rows.push(
['[', ']', '{', '}', '#', '%', '^', '*', '+', '=']
.iter()
.map(|c| sym(*c))
.collect(),
);
rows.push(
['_', '\\', '|', '~', '<', '>', '€', '£', '¥', '·']
.iter()
.map(|c| sym(*c))
.collect(),
);
let mut row3 = Vec::with_capacity(9);
row3.push(KeySpec {
width_units: 1.5,
cap: KeyCap::Text("123".to_string()),
action: KeyAction::Switch(Layer::Numbers),
kind: KeyKind::Utility,
});
for c in ['.', ',', '?', '!', '\''] {
row3.push(sym(c));
}
row3.push(KeySpec {
width_units: 1.5,
cap: KeyCap::Glyph(KeyGlyph::Backspace),
action: KeyAction::Backspace,
kind: KeyKind::Utility,
});
rows.push(row3);
rows.push(action_row(Layer::Symbols));
Layout { rows }
}
fn action_row(current: Layer) -> Vec<KeySpec> {
let (mode_label, mode_action) = match current {
Layer::Letters | Layer::Shifted => ("123", KeyAction::Switch(Layer::Numbers)),
Layer::Numbers | Layer::Symbols => ("ABC", KeyAction::Switch(Layer::Letters)),
};
vec![
KeySpec {
width_units: 1.5,
cap: KeyCap::Text(mode_label.to_string()),
action: mode_action,
kind: KeyKind::Utility,
},
KeySpec {
width_units: 1.0,
cap: KeyCap::Glyph(KeyGlyph::DismissDown),
action: KeyAction::Dismiss,
kind: KeyKind::Utility,
},
KeySpec {
width_units: 5.0,
cap: KeyCap::Text("space".to_string()),
action: KeyAction::Space,
kind: KeyKind::Letter,
},
KeySpec {
width_units: 2.0,
cap: KeyCap::Glyph(KeyGlyph::Return),
action: KeyAction::Enter,
kind: KeyKind::Return,
},
]
}
fn paint_key(
ctx: &mut dyn DrawCtx,
rect: Rect,
spec: &KeySpec,
pressed: bool,
style: &Style,
_active_layer: Layer,
) {
let (bg, text_color) = match (spec.kind, pressed) {
(KeyKind::Letter, false) => (style.key_face_bg, style.key_face_text),
(KeyKind::Letter, true) => (style.key_face_bg_pressed, style.key_face_text_pressed),
(KeyKind::Utility, false) => (style.util_key_bg, style.util_key_text),
(KeyKind::Utility, true) => (style.util_key_bg_pressed, style.key_face_text_pressed),
(KeyKind::Return, false) => (style.return_key_bg, style.return_key_text),
(KeyKind::Return, true) => (style.return_key_bg_pressed, style.return_key_text),
};
ctx.set_fill_color(style.key_shadow);
ctx.begin_path();
ctx.rounded_rect(
rect.x,
rect.y + style.key_shadow_offset_y,
rect.width,
rect.height,
style.key_corner_radius,
);
ctx.fill();
ctx.set_fill_color(bg);
ctx.begin_path();
ctx.rounded_rect(
rect.x,
rect.y,
rect.width,
rect.height,
style.key_corner_radius,
);
ctx.fill();
ctx.set_fill_color(text_color);
let center = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
match &spec.cap {
KeyCap::Text(text) => {
let font_size = if matches!(spec.kind, KeyKind::Letter) && text.chars().count() == 1 {
style.letter_font_size
} else {
style.utility_font_size
};
ctx.set_font_size(font_size);
let approx_width = text.chars().count() as f64 * font_size * 0.55;
ctx.fill_text(
text,
center.x - approx_width / 2.0,
center.y - font_size * 0.3,
);
}
KeyCap::Glyph(glyph) => {
paint_glyph(ctx, center, style, *glyph, text_color);
}
}
}
fn paint_glyph(
ctx: &mut dyn DrawCtx,
center: Point,
style: &Style,
glyph: super::key::KeyGlyph,
color: crate::color::Color,
) {
use super::key::KeyGlyph;
let r = style.utility_font_size * 0.55;
ctx.set_stroke_color(color);
ctx.set_fill_color(color);
ctx.set_line_width(2.0);
match glyph {
KeyGlyph::Backspace => {
ctx.begin_path();
ctx.move_to(center.x - r, center.y);
ctx.line_to(center.x - r * 0.4, center.y + r * 0.7);
ctx.line_to(center.x + r * 0.9, center.y + r * 0.7);
ctx.line_to(center.x + r * 0.9, center.y - r * 0.7);
ctx.line_to(center.x - r * 0.4, center.y - r * 0.7);
ctx.close_path();
ctx.stroke();
ctx.begin_path();
ctx.move_to(center.x - r * 0.05, center.y - r * 0.35);
ctx.line_to(center.x + r * 0.55, center.y + r * 0.35);
ctx.move_to(center.x - r * 0.05, center.y + r * 0.35);
ctx.line_to(center.x + r * 0.55, center.y - r * 0.35);
ctx.stroke();
}
KeyGlyph::Shift => {
ctx.begin_path();
ctx.move_to(center.x, center.y + r);
ctx.line_to(center.x - r, center.y);
ctx.line_to(center.x - r * 0.4, center.y);
ctx.line_to(center.x - r * 0.4, center.y - r * 0.6);
ctx.line_to(center.x + r * 0.4, center.y - r * 0.6);
ctx.line_to(center.x + r * 0.4, center.y);
ctx.line_to(center.x + r, center.y);
ctx.close_path();
ctx.stroke();
}
KeyGlyph::DismissDown => {
ctx.begin_path();
ctx.move_to(center.x - r, center.y + r * 0.3);
ctx.line_to(center.x, center.y - r * 0.3);
ctx.line_to(center.x + r, center.y + r * 0.3);
ctx.stroke();
ctx.begin_path();
ctx.move_to(center.x - r, center.y - r * 0.6);
ctx.line_to(center.x + r, center.y - r * 0.6);
ctx.stroke();
}
KeyGlyph::Return => {
ctx.begin_path();
ctx.move_to(center.x + r, center.y + r * 0.6);
ctx.line_to(center.x + r, center.y - r * 0.2);
ctx.line_to(center.x - r * 0.5, center.y - r * 0.2);
ctx.stroke();
ctx.begin_path();
ctx.move_to(center.x - r * 0.5, center.y + r * 0.3);
ctx.line_to(center.x - r, center.y - r * 0.2);
ctx.line_to(center.x - r * 0.5, center.y - r * 0.7);
ctx.stroke();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn letters_layer_has_four_rows() {
let l = Layout::for_layer(Layer::Letters);
assert_eq!(l.rows.len(), 4);
}
#[test]
fn shift_key_switches_layer() {
let l = Layout::for_layer(Layer::Letters);
let row3 = &l.rows[2];
let shift = &row3[0];
match shift.action {
KeyAction::Switch(Layer::Shifted) => {}
other => panic!("expected Switch(Shifted) on row3[0], got {other:?}"),
}
}
#[test]
fn shifted_layer_emits_uppercase_chars() {
let l = Layout::for_layer(Layer::Shifted);
let q = &l.rows[0][0];
match q.action {
KeyAction::Char('Q') => {}
other => panic!("expected Char('Q'), got {other:?}"),
}
}
#[test]
fn numbers_layer_includes_digits() {
let l = Layout::for_layer(Layer::Numbers);
let chars: Vec<char> = l.rows[0]
.iter()
.filter_map(|k| match k.action {
KeyAction::Char(c) => Some(c),
_ => None,
})
.collect();
for d in ['1', '2', '3', '0'] {
assert!(chars.contains(&d), "missing digit {d} in numbers row 1");
}
}
}