@@ -3,17 +3,35 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Padding, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{components::GraphExplorer, state::SharedState, Msg};
|
||||
use crate::{components::GraphExplorer, models::EditMsg, state::SharedState, Msg};
|
||||
|
||||
use self::dialog::{CreateItem, CreateItemState};
|
||||
|
||||
pub mod dialog;
|
||||
|
||||
pub enum Dialog {
|
||||
CreateItem { state: CreateItemState },
|
||||
}
|
||||
|
||||
pub enum Mode {
|
||||
View,
|
||||
Insert,
|
||||
}
|
||||
|
||||
pub struct App<'a> {
|
||||
state: SharedState,
|
||||
|
||||
pub mode: Mode,
|
||||
dialog: Option<Dialog>,
|
||||
|
||||
graph_explorer: GraphExplorer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(state: SharedState, graph_explorer: GraphExplorer<'a>) -> Self {
|
||||
Self {
|
||||
mode: Mode::View,
|
||||
dialog: None,
|
||||
state,
|
||||
graph_explorer,
|
||||
}
|
||||
@@ -23,10 +41,30 @@ impl<'a> App<'a> {
|
||||
tracing::trace!("handling msg: {:?}", msg);
|
||||
|
||||
match msg {
|
||||
Msg::MoveRight => self.graph_explorer.move_right(),
|
||||
Msg::MoveLeft => self.graph_explorer.move_left(),
|
||||
Msg::MoveDown => self.graph_explorer.move_down(),
|
||||
Msg::MoveUp => self.graph_explorer.move_up(),
|
||||
Msg::MoveRight => self.graph_explorer.move_right()?,
|
||||
Msg::MoveLeft => self.graph_explorer.move_left()?,
|
||||
Msg::MoveDown => self.graph_explorer.move_down()?,
|
||||
Msg::MoveUp => self.graph_explorer.move_up()?,
|
||||
Msg::OpenCreateItemDialog => self.open_dialog(),
|
||||
Msg::EnterInsertMode => self.mode = Mode::Insert,
|
||||
Msg::EnterCommandMode => self.mode = Mode::View,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(dialog) = &mut self.dialog {
|
||||
match dialog {
|
||||
Dialog::CreateItem { state } => state.update(&msg)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_dialog(&mut self) {
|
||||
if self.dialog.is_none() {
|
||||
self.dialog = Some(Dialog::CreateItem {
|
||||
state: CreateItemState::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,16 +84,40 @@ impl<'a> Widget for &mut App<'a> {
|
||||
}
|
||||
|
||||
pub fn render_app(frame: &mut Frame, state: &mut App) {
|
||||
let chunks =
|
||||
Layout::vertical(vec![Constraint::Length(2), Constraint::Min(0)]).split(frame.size());
|
||||
let chunks = Layout::vertical(vec![
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
let heading = Paragraph::new(text::Line::from(
|
||||
Span::styled("hyperlog", Style::default()).fg(Color::Green),
|
||||
));
|
||||
let mut heading_parts = vec![Span::styled("hyperlog", Style::default()).fg(Color::Green)];
|
||||
|
||||
if let Some(dialog) = &state.dialog {
|
||||
heading_parts.push(Span::raw(" ~ "));
|
||||
|
||||
match dialog {
|
||||
Dialog::CreateItem { .. } => heading_parts.push(Span::raw("create item")),
|
||||
}
|
||||
}
|
||||
|
||||
let heading = Paragraph::new(text::Line::from(heading_parts));
|
||||
let block_heading = Block::default().borders(Borders::BOTTOM);
|
||||
|
||||
frame.render_widget(heading.block(block_heading), chunks[0]);
|
||||
|
||||
let powerbar = match &state.mode {
|
||||
Mode::View => Line::raw("-- VIEW --"),
|
||||
Mode::Insert => Line::raw("-- EDIT --"),
|
||||
};
|
||||
let powerbar_block = Block::default()
|
||||
.borders(Borders::empty())
|
||||
.padding(Padding::new(1, 1, 0, 0));
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![powerbar]).block(powerbar_block),
|
||||
chunks[2],
|
||||
);
|
||||
|
||||
let Rect { width, height, .. } = chunks[1];
|
||||
|
||||
let height = height as usize;
|
||||
@@ -80,7 +142,16 @@ pub fn render_app(frame: &mut Frame, state: &mut App) {
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
});
|
||||
//frame.render_widget(background.block(bg_block), chunks[1]);
|
||||
|
||||
frame.render_widget(state, chunks[1])
|
||||
if let Some(dialog) = state.dialog.as_mut() {
|
||||
match dialog {
|
||||
Dialog::CreateItem { state } => {
|
||||
frame.render_stateful_widget(&mut CreateItem::default(), chunks[1], state)
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
frame.render_widget(state, chunks[1]);
|
||||
}
|
||||
|
242
crates/hyperlog-tui/src/app/dialog.rs
Normal file
242
crates/hyperlog-tui/src/app/dialog.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::models::{EditMsg, Msg};
|
||||
|
||||
pub enum BufferState {
|
||||
Focused {
|
||||
content: ropey::Rope,
|
||||
position: usize,
|
||||
},
|
||||
Static {
|
||||
content: String,
|
||||
position: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for BufferState {
|
||||
fn default() -> Self {
|
||||
Self::Static {
|
||||
content: String::new(),
|
||||
position: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferState {
|
||||
fn update(&mut self, msg: &EditMsg) -> anyhow::Result<()> {
|
||||
if let BufferState::Focused { content, position } = self {
|
||||
let pos = *position;
|
||||
|
||||
match msg {
|
||||
EditMsg::Delete => {
|
||||
if pos > 0 && content.len_chars() > pos {
|
||||
content.remove((pos - 1)..pos);
|
||||
*position = position.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
EditMsg::DeleteNext => {
|
||||
if pos > 0 && pos < content.len_chars() {
|
||||
content.remove((pos)..pos + 1);
|
||||
}
|
||||
}
|
||||
EditMsg::InsertNewLine => todo!(),
|
||||
EditMsg::InsertTab => todo!(),
|
||||
EditMsg::InsertChar(c) => {
|
||||
content.try_insert_char(pos, *c)?;
|
||||
*position = position.saturating_add(1);
|
||||
}
|
||||
EditMsg::MoveLeft => {
|
||||
*position = position.saturating_sub(1);
|
||||
}
|
||||
EditMsg::MoveRight => {
|
||||
if pos + 1 < content.len_chars() {
|
||||
*position = pos.saturating_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputBuffer {
|
||||
state: BufferState,
|
||||
}
|
||||
|
||||
impl InputBuffer {
|
||||
fn to_focused(&mut self) {
|
||||
match &mut self.state {
|
||||
BufferState::Focused { .. } => {}
|
||||
BufferState::Static { content, position } => {
|
||||
self.state = BufferState::Focused {
|
||||
content: ropey::Rope::from(content.as_str()),
|
||||
position: *position,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_static(&mut self) {
|
||||
match &mut self.state {
|
||||
BufferState::Focused { content, position } => {
|
||||
self.state = BufferState::Static {
|
||||
content: content.to_string(),
|
||||
position: *position,
|
||||
}
|
||||
}
|
||||
BufferState::Static { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self) {
|
||||
match &mut self.state {
|
||||
BufferState::Focused { content, position } => {
|
||||
self.state = BufferState::Static {
|
||||
content: content.to_string(),
|
||||
position: *position,
|
||||
}
|
||||
}
|
||||
BufferState::Static { content, position } => {
|
||||
self.state = BufferState::Focused {
|
||||
content: ropey::Rope::from(content.as_str()),
|
||||
position: *position,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: &Msg) -> anyhow::Result<()> {
|
||||
match msg {
|
||||
Msg::EnterInsertMode => self.to_focused(),
|
||||
Msg::EnterCommandMode => self.to_static(),
|
||||
Msg::Edit(c) => {
|
||||
self.state.update(c)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for InputBuffer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InputBuffer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: BufferState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputField<'a> {
|
||||
title: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> InputField<'a> {
|
||||
pub fn new(title: &'a str) -> Self {
|
||||
Self { title }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for InputField<'a> {
|
||||
type State = InputBuffer;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let block = Block::bordered().title(self.title);
|
||||
|
||||
match &state.state {
|
||||
BufferState::Focused { content, .. } => {
|
||||
Paragraph::new(content.to_string().as_str())
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
}
|
||||
BufferState::Static { content, .. } => {
|
||||
Paragraph::new(content.as_str())
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CreateItemFocused {
|
||||
Title,
|
||||
Description,
|
||||
}
|
||||
impl Default for CreateItemFocused {
|
||||
fn default() -> Self {
|
||||
Self::Title
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CreateItemState {
|
||||
title: InputBuffer,
|
||||
description: InputBuffer,
|
||||
|
||||
focused: CreateItemFocused,
|
||||
}
|
||||
|
||||
impl CreateItemState {
|
||||
pub fn update(&mut self, msg: &Msg) -> anyhow::Result<()> {
|
||||
match &msg {
|
||||
Msg::MoveDown | Msg::MoveUp => match self.focused {
|
||||
CreateItemFocused::Title => self.focused = CreateItemFocused::Description,
|
||||
CreateItemFocused::Description => self.focused = CreateItemFocused::Title,
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match self.focused {
|
||||
CreateItemFocused::Title => {
|
||||
self.title.update(msg)?;
|
||||
}
|
||||
CreateItemFocused::Description => {
|
||||
self.description.update(msg)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CreateItem {}
|
||||
|
||||
impl StatefulWidget for &mut CreateItem {
|
||||
// fn render(self, area: Rect, buf: &mut Buffer)
|
||||
// where
|
||||
// Self: Sized,
|
||||
// {
|
||||
// //buf.reset();
|
||||
|
||||
// // let block = Block::bordered()
|
||||
// // .title("create item")
|
||||
// // .padding(Padding::proportional(1));
|
||||
|
||||
// }
|
||||
|
||||
type State = CreateItemState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let chunks =
|
||||
Layout::vertical(vec![Constraint::Length(3), Constraint::Length(3)]).split(area);
|
||||
|
||||
InputField::new("title").render(chunks[0], buf, &mut state.title);
|
||||
InputField::new("description").render(chunks[1], buf, &mut state.description);
|
||||
|
||||
// let title = Paragraph::new("something"); //.block(block);
|
||||
|
||||
// title.render(area, buf);
|
||||
}
|
||||
}
|
@@ -237,7 +237,7 @@ impl<'a> StatefulWidget for GraphExplorer<'a> {
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let Rect { height, .. } = area;
|
||||
let height = height as usize;
|
||||
let _height = height as usize;
|
||||
|
||||
if let Some(graph) = &state.graph {
|
||||
let movement_graph: MovementGraph = graph.clone().into();
|
||||
|
@@ -5,7 +5,7 @@ use app::{render_app, App};
|
||||
use components::GraphExplorer;
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use hyperlog_core::state::State;
|
||||
use models::Msg;
|
||||
use models::{EditMsg, Msg};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use crate::{state::SharedState, terminal::TerminalInstance};
|
||||
@@ -67,21 +67,42 @@ fn update(
|
||||
) -> Result<UpdateConclusion> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(UpdateConclusion::new(true)),
|
||||
KeyCode::Char('l') => {
|
||||
app.update(Msg::MoveRight)?;
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.update(Msg::MoveLeft)?;
|
||||
}
|
||||
KeyCode::Char('j') => {
|
||||
app.update(Msg::MoveDown)?;
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.update(Msg::MoveUp)?;
|
||||
}
|
||||
_ => {}
|
||||
match &app.mode {
|
||||
app::Mode::View => match key.code {
|
||||
KeyCode::Char('q') => return Ok(UpdateConclusion::new(true)),
|
||||
KeyCode::Char('l') => {
|
||||
app.update(Msg::MoveRight)?;
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.update(Msg::MoveLeft)?;
|
||||
}
|
||||
KeyCode::Char('j') => {
|
||||
app.update(Msg::MoveDown)?;
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.update(Msg::MoveUp)?;
|
||||
}
|
||||
KeyCode::Char('a') => {
|
||||
app.update(Msg::OpenCreateItemDialog)?;
|
||||
app.update(Msg::EnterInsertMode)?;
|
||||
}
|
||||
KeyCode::Char('i') => {
|
||||
app.update(Msg::EnterInsertMode)?;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
app::Mode::Insert => match key.code {
|
||||
KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?,
|
||||
KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?,
|
||||
KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?,
|
||||
KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?,
|
||||
KeyCode::Char(c) => app.update(Msg::Edit(EditMsg::InsertChar(c)))?,
|
||||
KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?,
|
||||
KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?,
|
||||
KeyCode::Esc => app.update(Msg::EnterCommandMode)?,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,4 +4,20 @@ pub enum Msg {
|
||||
MoveLeft,
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
OpenCreateItemDialog,
|
||||
|
||||
EnterInsertMode,
|
||||
EnterCommandMode,
|
||||
Edit(EditMsg),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditMsg {
|
||||
Delete,
|
||||
InsertNewLine,
|
||||
InsertTab,
|
||||
DeleteNext,
|
||||
InsertChar(char),
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
}
|
||||
|
Reference in New Issue
Block a user