Merge pull request #3 from stevedylandev/chore/refactor-spacing
1aa01f05
chore: refactor spacing
1 file(s) · +51 −39
chore: refactor spacing
| 7 | 7 | layout::{Constraint, Direction, Layout}, |
|
| 8 | 8 | style::{Color, Modifier, Style}, |
|
| 9 | 9 | text::{Line, Span, Text}, |
|
| 10 | - | widgets::{Block, List, ListItem, ListState, Padding}, |
|
| 10 | + | widgets::{Block, Padding, Paragraph}, |
|
| 11 | 11 | }; |
|
| 12 | 12 | ||
| 13 | 13 | struct Item { |
|
| 207 | 207 | } |
|
| 208 | 208 | ||
| 209 | 209 | fn app(terminal: &mut DefaultTerminal, items: &[Item]) -> std::io::Result<()> { |
|
| 210 | - | let mut state = ListState::default(); |
|
| 211 | - | state.select(Some(0)); |
|
| 210 | + | let mut selected = 0; |
|
| 211 | + | let mut scroll_offset = 0; |
|
| 212 | 212 | ||
| 213 | 213 | loop { |
|
| 214 | - | terminal.draw(|f| render(f, items, &mut state))?; |
|
| 214 | + | terminal.draw(|f| render(f, items, selected, &mut scroll_offset))?; |
|
| 215 | 215 | ||
| 216 | 216 | if let crossterm::event::Event::Key(KeyEvent { code, .. }) = crossterm::event::read()? { |
|
| 217 | 217 | let len = items.len(); |
|
| 218 | 218 | match code { |
|
| 219 | 219 | KeyCode::Char('q') => break, |
|
| 220 | 220 | KeyCode::Char('j') | KeyCode::Down => { |
|
| 221 | - | let next = state |
|
| 222 | - | .selected() |
|
| 223 | - | .map(|i| (i + 1).min(len.saturating_sub(1))) |
|
| 224 | - | .unwrap_or(0); |
|
| 225 | - | state.select(Some(next)); |
|
| 221 | + | selected = (selected + 1).min(len.saturating_sub(1)); |
|
| 226 | 222 | } |
|
| 227 | 223 | KeyCode::Char('k') | KeyCode::Up => { |
|
| 228 | - | let prev = state.selected().map(|i| i.saturating_sub(1)).unwrap_or(0); |
|
| 229 | - | state.select(Some(prev)); |
|
| 224 | + | selected = selected.saturating_sub(1); |
|
| 230 | 225 | } |
|
| 231 | 226 | KeyCode::Enter => { |
|
| 232 | - | if let Some(i) = state.selected() { |
|
| 233 | - | if let Some(url) = items[i].url.as_deref() { |
|
| 234 | - | let _ = open::that(url); |
|
| 235 | - | } |
|
| 227 | + | if let Some(url) = items[selected].url.as_deref() { |
|
| 228 | + | let _ = open::that(url); |
|
| 236 | 229 | } |
|
| 237 | 230 | } |
|
| 238 | 231 | _ => {} |
|
| 254 | 247 | format!("{} {}{}, {}", dt.format("%B"), day, suffix, dt.format("%Y")) |
|
| 255 | 248 | } |
|
| 256 | 249 | ||
| 257 | - | fn render(frame: &mut Frame, items: &[Item], state: &mut ListState) { |
|
| 250 | + | fn render(frame: &mut Frame, items: &[Item], selected: usize, scroll_offset: &mut u16) { |
|
| 258 | 251 | let outer = frame.area(); |
|
| 259 | 252 | let [_, center, _] = Layout::default() |
|
| 260 | 253 | .direction(Direction::Horizontal) |
|
| 261 | 254 | .constraints([ |
|
| 262 | 255 | Constraint::Fill(1), |
|
| 263 | - | Constraint::Max(80), |
|
| 256 | + | Constraint::Max(100), |
|
| 264 | 257 | Constraint::Fill(1), |
|
| 265 | 258 | ]) |
|
| 266 | 259 | .areas(outer); |
|
| 274 | 267 | .fg(Color::DarkGray) |
|
| 275 | 268 | .add_modifier(Modifier::ITALIC); |
|
| 276 | 269 | ||
| 277 | - | let selected = state.selected(); |
|
| 278 | - | let list_items: Vec<ListItem> = items |
|
| 279 | - | .iter() |
|
| 280 | - | .enumerate() |
|
| 281 | - | .map(|(i, item)| { |
|
| 282 | - | let bar = if selected == Some(i) { "▌ " } else { " " }; |
|
| 283 | - | let mut lines = vec![Line::from(vec![ |
|
| 284 | - | Span::raw(bar), |
|
| 285 | - | Span::styled(item.date.clone(), dim), |
|
| 286 | - | ])]; |
|
| 287 | - | for wrapped in textwrap::wrap(&item.title, title_width.max(1)) { |
|
| 288 | - | lines.push(Line::from(vec![ |
|
| 289 | - | Span::raw(bar), |
|
| 290 | - | Span::raw(wrapped.into_owned()), |
|
| 291 | - | ])); |
|
| 292 | - | } |
|
| 270 | + | let mut selected_start = 0; |
|
| 271 | + | let mut selected_end = 0; |
|
| 272 | + | let mut lines = Vec::new(); |
|
| 273 | + | for (i, item) in items.iter().enumerate() { |
|
| 274 | + | if i == selected { |
|
| 275 | + | selected_start = lines.len(); |
|
| 276 | + | } |
|
| 277 | + | ||
| 278 | + | let bar = if i == selected { "▌ " } else { " " }; |
|
| 279 | + | lines.push(Line::from(vec![ |
|
| 280 | + | Span::raw(bar), |
|
| 281 | + | Span::styled(item.date.clone(), dim), |
|
| 282 | + | ])); |
|
| 283 | + | for wrapped in textwrap::wrap(&item.title, title_width.max(1)) { |
|
| 293 | 284 | lines.push(Line::from(vec![ |
|
| 294 | 285 | Span::raw(bar), |
|
| 295 | - | Span::styled(item.author.clone(), author_style), |
|
| 286 | + | Span::raw(wrapped.into_owned()), |
|
| 296 | 287 | ])); |
|
| 297 | - | lines.push(Line::from("")); |
|
| 288 | + | } |
|
| 289 | + | lines.push(Line::from(vec![ |
|
| 290 | + | Span::raw(bar), |
|
| 291 | + | Span::styled(item.author.clone(), author_style), |
|
| 292 | + | ])); |
|
| 293 | + | lines.push(Line::from("")); |
|
| 298 | 294 | ||
| 299 | - | ListItem::new(Text::from(lines)) |
|
| 300 | - | }) |
|
| 301 | - | .collect(); |
|
| 295 | + | if i == selected { |
|
| 296 | + | selected_end = lines.len(); |
|
| 297 | + | } |
|
| 298 | + | } |
|
| 299 | + | ||
| 300 | + | let viewport_height = inner.height as usize; |
|
| 301 | + | if viewport_height > 0 { |
|
| 302 | + | let mut offset = *scroll_offset as usize; |
|
| 303 | + | if selected_start < offset { |
|
| 304 | + | offset = selected_start; |
|
| 305 | + | } else if selected_end > offset + viewport_height { |
|
| 306 | + | offset = selected_end.saturating_sub(viewport_height); |
|
| 307 | + | } |
|
| 308 | + | offset = offset.min(lines.len().saturating_sub(viewport_height)); |
|
| 309 | + | *scroll_offset = offset.min(u16::MAX as usize) as u16; |
|
| 310 | + | } |
|
| 302 | 311 | ||
| 303 | 312 | frame.render_widget(block, center); |
|
| 304 | - | frame.render_stateful_widget(List::new(list_items).highlight_symbol(""), inner, state); |
|
| 313 | + | frame.render_widget( |
|
| 314 | + | Paragraph::new(Text::from(lines)).scroll((*scroll_offset, 0)), |
|
| 315 | + | inner, |
|
| 316 | + | ); |
|
| 305 | 317 | } |
|