use yakui::Alignment;
use yakui::geometry::{Constraints, Vec2};
use yakui::widgets::List;
use super::{GamepadState, ps};
use crate::ecs::{Application, Plugin};
use crate::ui;
const COLUMNS: usize = 30;
const FIXED_LINES: usize = 6;
const HELD_LINES: usize = 3;
const LINES: usize = FIXED_LINES + HELD_LINES;
const PX: f32 = 16.0;
pub fn show(pad: &GamepadState) {
let width = COLUMNS as f32 * PX * 0.62 + ui::PANEL_PADDING * 2.0;
ui::corner(Alignment::TOP_LEFT, || {
ui::panel_colored(yakui::geometry::Color::rgba(15, 15, 23, 204), || {
yakui::constrained(
Constraints {
min: Vec2::new(width, 0.0),
max: Vec2::new(width, f32::INFINITY),
},
|| {
let mut column = List::column();
column.main_axis_size = yakui::MainAxisSize::Min;
column.item_spacing = 4.0;
column.show(|| {
for line in readout(pad) {
let line = match line.is_empty() {
true => " ".to_string(),
false => line,
};
ui::text_colored(PX, line, yakui::geometry::Color::rgb(217, 224, 242));
}
});
},
);
});
});
}
fn readout(pad: &GamepadState) -> Vec<String> {
let mut lines = Vec::with_capacity(LINES);
let mut header = format!("PAD:{}", pad.name());
if let Some(battery) = pad.state.battery.filter(|_| pad.connected) {
let charge = match battery.charge {
ps::Charge::Discharging => "",
ps::Charge::Charging => " CHG",
ps::Charge::Full => " FULL",
ps::Charge::Error => " ERR",
};
header.push_str(&format!(" BATT:{}{}", battery.percent, charge));
}
lines.push(header);
if !pad.connected {
lines.resize(LINES, String::new());
return lines;
}
let state = &pad.state;
lines.push(format!(
"LX:{:3} LY:{:3} RX:{:3} RY:{:3}",
state.lx, state.ly, state.rx, state.ry
));
lines.push(format!(
"L2:{:3} R2:{:3} DPAD:{}",
state.l2,
state.r2,
state.dpad_name()
));
let touch = state
.touch
.iter()
.flatten()
.map(|touch| format!("{:04},{:04}", touch.x, touch.y))
.collect::<Vec<_>>();
lines.push(match touch.is_empty() {
true => "TOUCH:-".to_string(),
false => format!("TOUCH:{}", touch.join(" ")),
});
let (move_axis, look_axis) = (state.move_axis(), state.look_axis());
lines.push(format!(
"MOVE:{:+.2},{:+.2} LIFT:{:+.2}",
move_axis.x,
move_axis.y,
state.lift()
));
lines.push(format!("LOOK:{:+.2},{:+.2}", look_axis.x, look_axis.y));
let held = ps::button::NAMES
.iter()
.filter(|(mask, _)| state.held(*mask))
.map(|(_, name)| *name)
.collect::<Vec<_>>();
lines.extend(wrap(&held, COLUMNS - "HELD:".len(), "HELD:", HELD_LINES));
lines.resize(LINES, String::new());
lines
}
fn wrap(names: &[&str], columns: usize, prefix: &str, limit: usize) -> Vec<String> {
let indent = " ".repeat(prefix.len());
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
for name in names {
let needed = if current.is_empty() {
name.len()
} else {
current.len() + 1 + name.len()
};
if needed > columns && !current.is_empty() {
lines.push(std::mem::take(&mut current));
if lines.len() == limit {
break;
}
}
if !current.is_empty() {
current.push(' ');
}
current.push_str(name);
}
if lines.len() < limit && !current.is_empty() {
lines.push(current);
}
if lines.is_empty() {
lines.push("-".to_string());
}
lines
.into_iter()
.enumerate()
.map(|(i, line)| {
if i == 0 {
format!("{prefix}{line}")
} else {
format!("{indent}{line}")
}
})
.collect()
}
pub struct HidPlugin;
impl Plugin for HidPlugin {
fn build(&self, app: &mut Application) {
app.insert_resource(GamepadState::default());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn connected(state: ps::State) -> GamepadState {
GamepadState {
connected: true,
model: Some(ps::Model::DualSense),
state,
}
}
#[test]
fn there_is_one_string_per_line_whatever_is_held() {
assert_eq!(readout(&GamepadState::default()).len(), LINES);
assert_eq!(readout(&connected(ps::State::default())).len(), LINES);
let everything = ps::State {
buttons: u32::MAX,
..ps::State::default()
};
assert_eq!(readout(&connected(everything)).len(), LINES);
}
#[test]
fn an_absent_pad_says_so_and_leaves_the_rest_blank() {
let lines = readout(&GamepadState::default());
assert_eq!(lines[0], "PAD:NOT CONNECTED");
assert!(lines[1..].iter().all(String::is_empty), "{lines:?}");
}
#[test]
fn what_is_held_is_named() {
let state = ps::State {
buttons: ps::button::CROSS | ps::button::L1,
..ps::State::default()
};
let lines = readout(&connected(state));
assert_eq!(lines[FIXED_LINES], "HELD:X L1");
let lines = readout(&connected(ps::State::default()));
assert_eq!(lines[FIXED_LINES], "HELD:-", "nothing held still reads");
}
#[test]
fn the_touchpad_shows_both_fingers_and_says_so_when_there_are_none() {
let lines = readout(&connected(ps::State::default()));
assert_eq!(lines[3], "TOUCH:-");
let one = ps::State {
touch: [
Some(ps::Touch {
id: 1,
x: 12,
y: 340,
}),
None,
],
..ps::State::default()
};
assert_eq!(readout(&connected(one))[3], "TOUCH:0012,0340");
}
#[test]
fn nothing_ever_runs_off_the_panel() {
let everything = ps::State {
buttons: u32::MAX,
lx: 255,
ly: 255,
rx: 255,
ry: 255,
l2: 255,
r2: 255,
dpad: 1,
battery: Some(ps::Battery {
percent: 100,
charge: ps::Charge::Full,
}),
touch: [
Some(ps::Touch {
id: 1,
x: 1919,
y: 1079,
}),
Some(ps::Touch {
id: 2,
x: 1919,
y: 1079,
}),
],
};
for line in readout(&connected(everything)) {
assert!(line.len() <= COLUMNS, "{} chars: {line:?}", line.len());
}
}
#[test]
fn a_wrapped_list_keeps_every_name_and_lines_up_under_the_prefix() {
let names = ["AAAA", "BBBB", "CCCC"];
let lines = wrap(&names, 9, "HELD:", 3);
assert_eq!(
lines,
vec!["HELD:AAAA BBBB".to_string(), " CCCC".to_string()]
);
}
}