use crate::game::{board::Pos, line::Dir, moves::Move, rules::TouchMode, state::GameState};
use egui::{Color32, Pos2, Sense, Stroke, Ui, Vec2};
const CELL_PAD: f32 = 1.6;
const BG: Color32 = Color32::from_rgb(0xf7, 0xf7, 0xf4);
const INK: Color32 = Color32::from_rgb(0x15, 0x16, 0x1c);
const WHITE: Color32 = Color32::from_rgb(0xff, 0xff, 0xff);
const GOLD: Color32 = Color32::from_rgb(0xff, 0xd2, 0x3f);
fn view_pos(rot: u8, flip: bool, (x, y): Pos) -> Pos {
let (x, y) = if flip { (-x, y) } else { (x, y) };
match rot % 4 {
0 => (x, y),
1 => (-y, x),
2 => (-x, -y),
_ => (y, -x),
}
}
#[allow(clippy::too_many_arguments)]
pub fn show(
ui: &mut Ui,
state: &GameState,
legal: &[Move],
hovered: &mut Option<Move>,
view_rot: u8,
view_flip: bool,
view_arrows: bool,
view_numbers: bool,
show_legal: bool,
view_zoom: &mut f32,
view_pan: &mut Vec2,
dark: bool,
) -> Option<Move> {
let available = ui.available_rect_before_wrap();
let vt = |p: Pos| view_pos(view_rot, view_flip, p);
let (bg, ink, played_fill) = if dark {
(
Color32::from_rgb(0x1b, 0x1c, 0x22),
Color32::from_rgb(0xe6, 0xe6, 0xe9),
Color32::from_rgb(0x2a, 0x2b, 0x33),
)
} else {
(BG, INK, WHITE)
};
let (omin_x, omin_y, omax_x, omax_y) = state.bounding_box().unwrap_or((-5, -4, 6, 5));
let corners = [
vt((omin_x, omin_y)),
vt((omin_x, omax_y)),
vt((omax_x, omin_y)),
vt((omax_x, omax_y)),
];
let min_x = corners.iter().map(|c| c.0).min().unwrap();
let max_x = corners.iter().map(|c| c.0).max().unwrap();
let min_y = corners.iter().map(|c| c.1).min().unwrap();
let max_y = corners.iter().map(|c| c.1).max().unwrap();
let span_x = (max_x - min_x + 1) as f32 + 2.0 * CELL_PAD;
let span_y = (max_y - min_y + 1) as f32 + 2.0 * CELL_PAD;
let (resp, painter) = ui.allocate_painter(available.size(), Sense::click_and_drag());
let (scroll, zoom_mod) = ui.input(|i| {
let m = i.modifiers;
(i.raw_scroll_delta.y, m.ctrl || m.shift || m.command)
});
let cycle_scroll = if resp.hovered() && !zoom_mod {
scroll
} else {
0.0
};
if resp.hovered() && zoom_mod && scroll != 0.0 {
let factor = (scroll * 0.0015).exp();
let new_zoom = (*view_zoom * factor).clamp(0.4, 12.0);
if let Some(p) = resp.hover_pos() {
let c = available.center().to_vec2() + *view_pan;
*view_pan += (p.to_vec2() - c) * (1.0 - new_zoom / *view_zoom);
}
*view_zoom = new_zoom;
}
if resp.dragged() {
*view_pan += resp.drag_delta();
}
let fit = (available.width() / span_x).min(available.height() / span_y);
let cell_size = (fit * *view_zoom).max(3.0);
let cx = available.center().x + view_pan.x;
let cy = available.center().y + view_pan.y;
let board_cx = (min_x as f32 + max_x as f32) / 2.0;
let board_cy = (min_y as f32 + max_y as f32) / 2.0;
let to_screen = |pos: Pos| -> Pos2 {
let p = vt(pos);
Pos2::new(
cx + (p.0 as f32 - board_cx) * cell_size,
cy + (p.1 as f32 - board_cy) * cell_size,
)
};
painter.rect_filled(available, 0.0, bg);
let line_w = (cell_size * 0.085).max(1.2);
let dot_r = (cell_size * 0.26).max(2.6);
for mv in &state.history {
let pts: Vec<Pos2> = mv
.line
.positions(state.variant.len())
.map(&to_screen)
.collect();
let c = dir_color(mv.line.dir, 255);
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(line_w, c));
}
}
let n = state.variant.len() as i16;
let mid = (state.variant.len() as usize - 1) / 2;
if view_arrows {
for mv in &state.history {
let (dx, dy) = mv.line.dir.delta();
let (tdx, tdy) = vt((dx, dy));
let len = ((tdx * tdx + tdy * tdy) as f32).sqrt().max(1.0);
let (ux, uy) = (tdx as f32 / len, tdy as f32 / len);
let (perpx, perpy) = (-uy, ux);
let color = dir_color(mv.line.dir, 255);
if matches!(state.variant.touch_mode, TouchMode::Touching) {
let origin = mv.line.origin;
let far = (origin.0 + (n - 1) * dx, origin.1 + (n - 1) * dy);
let lp = mv.line_pos as i16;
let tw = 0.22 * cell_size;
for (end, into, at) in [(origin, 1.0f32, 0i16), (far, -1.0f32, n - 1)] {
if lp == at {
continue;
}
let e = to_screen(end);
let (tx, ty) = (e.x + into * ux * dot_r, e.y + into * uy * dot_r);
painter.line_segment(
[
Pos2::new(tx + perpx * tw, ty + perpy * tw),
Pos2::new(tx - perpx * tw, ty - perpy * tw),
],
Stroke::new((line_w * 2.0).max(2.0), color),
);
}
}
let sgn = if (mv.line_pos as usize) <= mid {
1.0
} else {
-1.0
};
let m = to_screen(mv.pos);
let (bx, by) = (m.x + sgn * ux * dot_r, m.y + sgn * uy * dot_r);
let (tlen, hw) = (0.30 * cell_size, 0.18 * cell_size);
painter.add(egui::Shape::convex_polygon(
vec![
Pos2::new(bx + sgn * ux * tlen, by + sgn * uy * tlen),
Pos2::new(bx + perpx * hw, by + perpy * hw),
Pos2::new(bx - perpx * hw, by - perpy * hw),
],
color,
Stroke::NONE,
));
}
}
let hover_pos = resp.hover_pos();
let nearest_pos = hover_pos.and_then(|hp| {
legal
.iter()
.map(|mv| (mv.pos, to_screen(mv.pos).distance(hp)))
.filter(|(_, d)| *d < cell_size * 0.5)
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(pos, _)| pos)
});
*hovered = None;
if let (Some(hp), Some(pos)) = (hover_pos, nearest_pos) {
let pt = to_screen(pos);
let off = hp - pt;
let mut cands: Vec<Move> = legal.iter().filter(|m| m.pos == pos).copied().collect();
cands.sort_by_key(|m| (m.line.dir.delta(), m.line.origin));
if cands.len() >= 2 {
let sw = (cell_size * 0.045).max(1.0);
for mv in &cands {
let pts: Vec<Pos2> = mv
.line
.positions(state.variant.len())
.map(&to_screen)
.collect();
let col = dir_color(mv.line.dir, 70);
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(sw, col));
}
}
}
let aim = |mv: &Move| {
mv.line
.positions(state.variant.len())
.map(|c| {
let s = to_screen(c);
off.x * (s.x - pt.x) + off.y * (s.y - pt.y)
})
.fold(f32::NEG_INFINITY, f32::max)
};
let aim_idx = (0..cands.len())
.max_by(|&i, &j| aim(&cands[i]).partial_cmp(&aim(&cands[j])).unwrap())
.unwrap_or(0);
let id = egui::Id::new("hover_cycle_sel");
let prev: Option<(Pos, Option<usize>, f32)> = ui.ctx().memory(|m| m.data.get_temp(id));
let (mut sel, mut accum) = match prev {
Some((p, s, a)) if p == pos => (s, a),
_ => (None, 0.0),
};
if cands.len() >= 2 && cycle_scroll != 0.0 {
const STEP: f32 = 40.0;
let n = cands.len();
accum += cycle_scroll;
while accum >= STEP {
sel = Some((sel.unwrap_or(aim_idx) + 1) % n);
accum -= STEP;
}
while accum <= -STEP {
sel = Some((sel.unwrap_or(aim_idx) + n - 1) % n);
accum += STEP;
}
}
ui.ctx()
.memory_mut(|m| m.data.insert_temp(id, (pos, sel, accum)));
let active = sel.unwrap_or(aim_idx).min(cands.len().saturating_sub(1));
*hovered = cands.get(active).copied();
if cands.len() >= 2 {
painter.text(
pt + egui::vec2(dot_r + 2.0, -dot_r - 2.0),
egui::Align2::LEFT_BOTTOM,
format!("{}/{}", active + 1, cands.len()),
egui::FontId::proportional((cell_size * 0.26).max(9.0)),
ink,
);
}
}
if let Some(mv) = *hovered {
let pts: Vec<Pos2> = mv
.line
.positions(state.variant.len())
.map(&to_screen)
.collect();
let c = dir_color(mv.line.dir, 150);
let pw = (cell_size * 0.16).max(2.0);
for w in pts.windows(2) {
painter.line_segment([w[0], w[1]], Stroke::new(pw, c));
}
painter.circle_filled(
to_screen(mv.pos),
(cell_size * 0.22).max(4.0),
Color32::from_rgb(0x2f, 0x9e, 0x44),
);
}
if show_legal {
let mr = (cell_size * 0.18).max(3.0);
for &mv in legal {
if Some(mv) == *hovered {
continue;
}
painter.circle_stroke(
to_screen(mv.pos),
mr,
Stroke::new(1.5_f32, Color32::from_rgba_unmultiplied(47, 158, 68, 130)),
);
}
}
let last_pos = state.history.last().map(|m| m.pos);
let played: std::collections::HashSet<_> = state.history.iter().map(|m| m.pos).collect();
let outline = Stroke::new((line_w * 0.9).max(1.0), ink);
for &cell in &state.board.cells {
let sp = to_screen(cell);
let fill = if last_pos == Some(cell) {
GOLD
} else if played.contains(&cell) {
played_fill
} else {
ink };
painter.circle(sp, dot_r, fill, outline);
}
if view_numbers {
let fsize = (cell_size * 0.30).max(5.0);
let font = egui::FontId::proportional(fsize);
for (i, mv) in state.history.iter().enumerate() {
painter.text(
to_screen(mv.pos),
egui::Align2::CENTER_CENTER,
(i + 1).to_string(),
font.clone(),
ink,
);
}
}
if resp.clicked() {
if let Some(mv) = *hovered {
return Some(mv);
}
}
None
}
fn dir_color(dir: Dir, alpha: u8) -> Color32 {
let (r, g, b) = match dir {
Dir::H => (0x3b, 0x62, 0xc4), Dir::V => (0xc8, 0x63, 0x2e), Dir::DP => (0x2f, 0x9e, 0x44), Dir::DN => (0xb5, 0x35, 0x9c), };
Color32::from_rgba_unmultiplied(r, g, b, alpha)
}