From addcfff12a76f861e07d844eabfa349e2f4014c1 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sun, 25 Aug 2024 02:22:37 -0400 Subject: Initial Commit --- src/widgets.rs | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/widgets.rs (limited to 'src/widgets.rs') diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..52eff3c --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,181 @@ +//! bevy_terminal_display widgets for dialog boxes + +use bevy::prelude::*; +use bevy_terminal_display::{crossterm, ratatui::{layout::{Alignment, Constraint, Flex, Layout}, style::Style, text::{Line, Span}, widgets::{Block, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap}}, widgets::TerminalWidget}; +use yarnspinner::runtime::DialogueOption; +use unicode_segmentation::UnicodeSegmentation as _; +use arbitrary_chunks::ArbitraryChunks as _; + +/// Interaction tooltip widget marker +// TODO: Move tooltip out of this crate? +#[derive(Component)] +pub struct InteractTooltip; + +/// Interaction tooltip widget +pub struct InteractTooltipWidget; + +impl TerminalWidget for InteractTooltipWidget { + fn render( + &mut self, + frame: &mut bevy_terminal_display::ratatui::Frame, + rect: bevy_terminal_display::ratatui::prelude::Rect, + ) { + let text = Paragraph::new("E") + .block( + Block::new() + .borders(Borders::ALL) + .padding(Padding::horizontal(1)), + ) + .alignment(Alignment::Center); + let [area] = Layout::horizontal([Constraint::Length(5)]) + .flex(Flex::Center) + .areas(rect); + let [area] = Layout::vertical([Constraint::Length(3)]) + .flex(Flex::Center) + .areas(area); + frame.render_widget(Clear, area); + frame.render_widget(text, area); + } +} + +/// Dialog box widget marker +#[derive(Component)] +pub struct DialogBox; + +/// Dialog box widget +#[derive(Default)] +pub struct DialogBoxWidget { + /// Name of speaking character + pub character: Option, + /// Chunks of text and corresponding styles representing the currently spoken line + pub text: Vec<(String, Style)>, +} + +impl TerminalWidget for DialogBoxWidget { + fn render( + &mut self, + frame: &mut bevy_terminal_display::ratatui::Frame, + rect: bevy_terminal_display::ratatui::prelude::Rect, + ) { + let text = Paragraph::new(bevy_terminal_display::ratatui::text::Line::from( + self.text + .iter() + .map(|(text, style)| Span::styled(text, *style)) + .collect::>(), + )) + .wrap(Wrap { trim: true }) + .block({ + let mut block = Block::new() + .borders(Borders::ALL) + .padding(Padding::horizontal(1)); + if let Some(character) = &self.character { + block = block.title(character.clone()); + } + block + }); + let [area] = Layout::horizontal([Constraint::Max(100)]) + .flex(Flex::Center) + .areas(rect); + let [area] = Layout::vertical([Constraint::Max(10)]) + .flex(Flex::End) + .areas(area); + frame.render_widget(Clear, area); + frame.render_widget(text, area); + } +} + +/// Option selection box widget marker +#[derive(Component)] +pub struct OptionsBox; + +/// Option selection box widget +#[derive(Default)] +pub struct OptionsBoxWidget { + /// State of ratatui list widget + pub state: ListState, + /// Available dialog options + pub options: Vec<(DialogueOption, Vec<(String, Style)>)>, +} + +impl TerminalWidget for OptionsBoxWidget { + fn render( + &mut self, + frame: &mut bevy_terminal_display::ratatui::Frame, + rect: bevy_terminal_display::ratatui::prelude::Rect, + ) { + let terminal_size = crossterm::terminal::size().unwrap(); + let box_size: u16 = if terminal_size.0 > 40 { 20 } else { 10 }; + let items = self + .options + .iter() + .map(|option| { + let option_spans = option + .1 + .iter() + .map(|(text, style)| Span::styled(text, *style)) + .collect::>(); + let string_chunks = textwrap::wrap( + &Line::from(option_spans.clone()).to_string(), + textwrap::Options::new(box_size as usize), + ) + .into_iter() + .map(|chunk| chunk.graphemes(true).count()) + .collect::>(); + + let line = Line::from(option_spans); + let graphemes = line.styled_graphemes(Style::default()).collect::>(); + + let mut final_string_chunks = vec![]; + let mut i = 0; + for string_chunk in string_chunks { + final_string_chunks.push(string_chunk); + i += string_chunk; + if let Some(grapheme) = graphemes.get(i) { + if grapheme.symbol == " " { + final_string_chunks.push(1); + i += 1; + } + } + } + + let mut chunked_graphemes = graphemes + .arbitrary_chunks(&final_string_chunks) + .collect::>(); + chunked_graphemes.retain(|chunk| !(chunk.len() == 1 && chunk[0].symbol == " ")); + + let line = chunked_graphemes + .into_iter() + .map(|chunk| { + Line::from( + chunk + .iter() + .map(|grapheme| { + Span::styled(grapheme.symbol.to_string(), grapheme.style) + }) + .collect::>(), + ) + }) + .collect::>(); + ListItem::new(line) + }) + .collect::>(); + + let height = items.iter().fold(0, |acc, item| acc + item.height()); + + let outer_block = Block::bordered().padding(Padding::horizontal(1)); + let list = List::new(items) + .block(outer_block) + .highlight_symbol("-> ") + .highlight_spacing(HighlightSpacing::Always); + + let [area] = Layout::horizontal([Constraint::Length(box_size + 7)]) + .flex(Flex::Center) + .areas(rect); + let [area] = Layout::vertical([Constraint::Length(height as u16 + 2)]) + .flex(Flex::Center) + .areas(area); + + frame.render_widget(Clear, area); + frame.render_stateful_widget(list, area, &mut self.state); + } +} -- cgit v1.2.3