Board
Group: Data
Code
pub struct BoardDemo {
models: Vec<BoardModel>,
last_message: String,
}
impl BoardDemo {
pub fn new() -> Self {
Self {
models: vec![sprint_board(), empty_board()],
last_message: "j/k=move h/l=col g/G=top/bottom".into(),
}
}
fn model(&self, variant: usize) -> &BoardModel {
&self.models[variant.min(self.models.len() - 1)]
}
fn model_mut(&mut self, variant: usize) -> &mut BoardModel {
let idx = variant.min(self.models.len() - 1);
&mut self.models[idx]
}
fn board_rect(area: Rect, backend: &dyn Backend) -> Rect {
let lh = backend.line_height();
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0))
}
fn status_rect(area: Rect, backend: &dyn Backend) -> Rect {
let lh = backend.line_height();
Rect::new(area.x, area.y + area.height - lh, area.width, lh)
}
fn status(&self) -> StatusBar {
StatusBar {
id: WidgetId::new("gallery:board:status"),
left_segments: vec![StatusBarSegment {
text: format!(" {} ", self.last_message),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(40, 80, 120),
bold: false,
action_id: None,
}],
right_segments: vec![],
}
}
}
impl Default for BoardDemo {
fn default() -> Self {
Self::new()
}
}
fn key_to_board_action(key: &Key) -> Option<BoardAction> {
match key {
Key::Char('j') => Some(BoardAction::MoveSelection(MoveDir::Down)),
Key::Char('k') => Some(BoardAction::MoveSelection(MoveDir::Up)),
Key::Char('h') => Some(BoardAction::MoveSelection(MoveDir::Left)),
Key::Char('l') => Some(BoardAction::MoveSelection(MoveDir::Right)),
Key::Char('g') => Some(BoardAction::JumpToTop),
Key::Char('G') => Some(BoardAction::JumpToBottom),
_ => None,
}
}
fn badge(label: &str, status: BadgeStatus) -> CardBadge {
CardBadge {
label: label.into(),
status,
}
}
fn sprint_board() -> BoardModel {
let backlog = BoardColumn {
id: WidgetId::new("gallery:board:col:backlog"),
title: "Backlog".into(),
cards: vec![
BoardCard {
id: WidgetId::new("gallery:board:card:1"),
title: "Improve search indexing perf".into(),
labels: vec!["perf".into()],
badges: vec![
badge("P", BadgeStatus::Passed),
badge("W", BadgeStatus::Pending),
],
hint: None,
},
BoardCard {
id: WidgetId::new("gallery:board:card:2"),
title: "Fix memory leak in parser".into(),
labels: vec!["bug".into()],
badges: vec![],
hint: None,
},
],
scroll_offset: 0,
};
let in_progress = BoardColumn {
id: WidgetId::new("gallery:board:col:in-progress"),
title: "In Progress".into(),
cards: vec![BoardCard {
id: WidgetId::new("gallery:board:card:3"),
title: "Upgrade dependency stack".into(),
labels: vec!["deps".into()],
badges: vec![
badge("P", BadgeStatus::Passed),
badge("W", BadgeStatus::Passed),
badge("T", BadgeStatus::Blocked),
],
hint: Some("blocked: upstream async-std 2.0 compat".into()),
}],
scroll_offset: 0,
};
let review = BoardColumn {
id: WidgetId::new("gallery:board:col:review"),
title: "Review".into(),
cards: vec![BoardCard {
id: WidgetId::new("gallery:board:card:4"),
title: "Paginate API list endpoints".into(),
labels: vec!["api".into()],
badges: vec![
badge("P", BadgeStatus::Passed),
badge("W", BadgeStatus::Passed),
badge("R", BadgeStatus::Warning),
],
hint: Some("reviewer: needs integration test".into()),
}],
scroll_offset: 0,
};
BoardModel {
id: WidgetId::new("gallery:board:main"),
columns: vec![backlog, in_progress, review],
selected_card_id: Some(WidgetId::new("gallery:board:card:3")),
col_scroll_offset: 0,
}
}
fn empty_board() -> BoardModel {
BoardModel {
id: WidgetId::new("gallery:board:empty"),
columns: vec![
BoardColumn {
id: WidgetId::new("gallery:board:empty:todo"),
title: "To Do".into(),
cards: vec![],
scroll_offset: 0,
},
BoardColumn {
id: WidgetId::new("gallery:board:empty:done"),
title: "Done".into(),
cards: vec![],
scroll_offset: 0,
},
],
selected_card_id: None,
col_scroll_offset: 0,
}
}
impl Demo for BoardDemo {
fn name(&self) -> &'static str {
"Board"
}
fn group(&self) -> &'static str {
"Data"
}
fn variants(&self) -> &'static [&'static str] {
&["Sprint board", "Empty board"]
}
fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
let board_rect = Self::board_rect(area, backend);
let _ = backend.draw_board(board_rect, self.model(variant));
let status_rect = Self::status_rect(area, backend);
let _ = backend.draw_status_bar_interactive(
status_rect,
&self.status(),
&InteractionState::new(),
);
}
fn handle(
&mut self,
variant: usize,
event: &UiEvent,
backend: &mut dyn Backend,
area: Rect,
) -> Reaction {
match event {
UiEvent::KeyPressed { key, .. } => {
let Some(action) = key_to_board_action(key) else {
return Reaction::Continue;
};
let model = self.model_mut(variant);
match action {
BoardAction::MoveSelection(dir) => model.move_selection(dir),
BoardAction::JumpToTop => model.jump_to_top(),
BoardAction::JumpToBottom => model.jump_to_bottom(),
_ => {}
}
Reaction::Redraw
}
UiEvent::MouseDown { position, .. } => {
let board_rect = Self::board_rect(area, backend);
let layout = backend.board_layout(board_rect, self.model(variant));
match layout.hit_test(position.x, position.y) {
BoardHit::Card(id) => {
let title = self
.model(variant)
.columns
.iter()
.find_map(|col| {
col.cards
.iter()
.find(|c| c.id == id)
.map(|c| c.title.clone())
})
.unwrap_or_default();
self.model_mut(variant).selected_card_id = Some(id);
self.last_message = format!("Selected: {title}");
Reaction::Redraw
}
BoardHit::ColumnHeader(_) | BoardHit::Empty => Reaction::Continue,
}
}
_ => Reaction::Continue,
}
}
fn source(&self) -> &'static str {
extract_region(SOURCE)
}
fn data(&self, variant: usize) -> serde_json::Value {
serde_json::to_value(self.model(variant)).unwrap_or(serde_json::Value::Null)
}
fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
None
}
}
Backends
| Variant | TUI | GTK | macOS | Windows |
|---|---|---|---|---|
| Sprint board | ||||
| Empty board |