fix(web client): reactive exit sign

This commit is contained in:
Henri Bourcereau 2026-05-07 14:27:53 +02:00
parent 7395d140cc
commit a82169fbe5

View file

@ -312,13 +312,8 @@ pub fn Board(
exit_field_test = |f| matches!(f, 1..=6); exit_field_test = |f| matches!(f, 1..=6);
} }
// Show a clickable exit sign outside the board when bearing off is possible. // Sequences clone for the reactive exit button (show/hide + class + click).
let has_exit_move = valid_sequences let seqs_exit = valid_sequences.clone();
.iter()
.any(|(m1, m2)| m1.get_to() == 0 || m2.get_to() == 0);
let show_exit_btn = all_in_exit && is_move_stage && has_exit_move;
let seqs_exit_cls = valid_sequences.clone();
let seqs_exit_click = valid_sequences.clone();
// `valid_sequences` is cloned per field (the Vec is small; Send-safe unlike Rc). // `valid_sequences` is cloned per field (the Vec is small; Send-safe unlike Rc).
let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> { let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> {
@ -624,70 +619,88 @@ pub fn Board(
</svg> </svg>
// Exit sign: circle+arrow outside the board, next to the last exit field. // Exit sign: circle+arrow outside the board, next to the last exit field.
// White exits to the right (top-right quarter); Black exits to the left (top-left). // White exits to the right (top-right quarter); Black exits to the left (top-left).
{show_exit_btn.then(|| { {move || {
let (pos_style, line_x1, line_x2, head_pts): (&str, &str, &str, &str) = // Recompute on every staged_moves change: the exit button must appear
if is_white { // even when the initial board has a checker outside the exit zone,
( // because the first move can bring all checkers in (e.g. 15→21, 19→exit).
"position:absolute;right:-60px;top:15px;width:50px;height:50px", let staged = staged_moves.get();
"10", "31", "23,17 32,25 23,33", let show = is_move_stage && match staged.len() {
) 0 => seqs_exit.iter().any(|(m1, m2)| m1.get_to() == 0 || m2.get_to() == 0),
} else { 1 => {
( let (f0, t0) = staged[0];
"position:absolute;left:-60px;top:15px;width:50px;height:50px", seqs_exit.iter()
"40", "19", "27,17 18,25 27,33", .filter(|(m1, _)| m1.get_from() as u8 == f0 && m1.get_to() as u8 == t0)
) .any(|(_, m2)| m2.get_to() == 0)
}; }
view! { _ => false,
<div };
title="Exit" show.then(|| {
style=pos_style let seqs_exit_cls = seqs_exit.clone();
class=move || { let seqs_exit_click = seqs_exit.clone();
let staged = staged_moves.get(); let (pos_style, line_x1, line_x2, head_pts): (&str, &str, &str, &str) =
let sel = selected_origin.get(); if is_white {
let active = match sel { (
Some(origin) => seqs_exit_cls.is_empty() "position:absolute;right:-60px;top:15px;width:50px;height:50px",
|| valid_dests_for(&seqs_exit_cls, &staged, origin) "10", "31", "23,17 32,25 23,33",
.iter() )
.any(|&d| d == 0), } else {
None => false, (
}; "position:absolute;left:-60px;top:15px;width:50px;height:50px",
if active { "exit-btn exit-active" } else { "exit-btn" } "40", "19", "27,17 18,25 27,33",
} )
on:click=move |_| { };
if !is_move_stage { return; } view! {
let staged = staged_moves.get_untracked(); <div
if staged.len() >= 2 { return; } title="Exit"
let Some(origin) = selected_origin.get_untracked() else { style=pos_style
return; class=move || {
}; let staged = staged_moves.get();
let valid = seqs_exit_click.is_empty() let sel = selected_origin.get();
|| valid_dests_for(&seqs_exit_click, &staged, origin) let active = match sel {
.iter() Some(origin) => seqs_exit_cls.is_empty()
.any(|&d| d == 0); || valid_dests_for(&seqs_exit_cls, &staged, origin)
if valid { .iter()
staged_moves.update(|v| v.push((origin, 0))); .any(|&d| d == 0),
selected_origin.set(None); None => false,
};
if active { "exit-btn exit-active" } else { "exit-btn" }
} }
} on:click=move |_| {
> if !is_move_stage { return; }
<svg width="50" height="50" viewBox="0 0 50 50"> let staged = staged_moves.get_untracked();
<circle if staged.len() >= 2 { return; }
cx="25" cy="25" r="20" let Some(origin) = selected_origin.get_untracked() else {
style="fill:rgba(10,20,10,0.75);stroke:rgba(210,170,30,0.75);stroke-width:2.5" return;
/> };
<line let valid = seqs_exit_click.is_empty()
x1=line_x1 y1="25" x2=line_x2 y2="25" || valid_dests_for(&seqs_exit_click, &staged, origin)
style="stroke:rgba(210,170,30,0.85);stroke-width:2.5;stroke-linecap:round" .iter()
/> .any(|&d| d == 0);
<polyline if valid {
points=head_pts staged_moves.update(|v| v.push((origin, 0)));
style="fill:none;stroke:rgba(210,170,30,0.85);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round" selected_origin.set(None);
/> }
</svg> }
</div> >
} <svg width="50" height="50" viewBox="0 0 50 50">
.into_any() <circle
})} cx="25" cy="25" r="20"
style="fill:rgba(10,20,10,0.75);stroke:rgba(210,170,30,0.75);stroke-width:2.5"
/>
<line
x1=line_x1 y1="25" x2=line_x2 y2="25"
style="stroke:rgba(210,170,30,0.85);stroke-width:2.5;stroke-linecap:round"
/>
<polyline
points=head_pts
style="fill:none;stroke:rgba(210,170,30,0.85);stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round"
/>
</svg>
</div>
}
.into_any()
})
}}
</div> </div>
<div class="zone-labels-row"> <div class="zone-labels-row">
<div class="zone-label zone-label-quarter">{label_bl}</div> <div class="zone-label zone-label-quarter">{label_bl}</div>