use anyhow::Context;
use arboard::Clipboard;
use indexmap::IndexMap;
use ratatui::layout::Rect;
use super::action::AppAction;
use super::util::copy_to_clipboard;
use super::view::{ActivePane, Pane, SideEffect, init_panes};
use crate::parser::{Image, Layer, LayerChangeSet, Sha256Digest};
use crate::tui::util::split_layout;
pub trait Store {
type Action;
fn handle(&mut self, action: Self::Action) -> anyhow::Result<()>;
}
pub struct AppState {
pub panes: [(Option<Pane>, Rect); 4],
pub command_bar_area: Rect,
pub active_pane: ActivePane,
pub clipboard: Option<Clipboard>,
pub layers: IndexMap<Sha256Digest, Layer>,
pub show_help_popup: bool,
pub is_in_insert_mode: bool,
}
impl AppState {
pub fn new(mut image: Image) -> anyhow::Result<Self> {
let panes = init_panes(&mut image).context("failed to init panes")?;
let clipboard = match Clipboard::new() {
Ok(clipboard) => Some(clipboard),
Err(e) => {
tracing::warn!("clipboard support is unavailable: {}", e);
None
}
};
Ok(AppState {
panes,
active_pane: ActivePane::default(),
command_bar_area: Rect::ZERO,
clipboard,
layers: image.layers,
show_help_popup: false,
is_in_insert_mode: false,
})
}
pub fn get_selected_layer(
&self,
) -> anyhow::Result<(&Sha256Digest, &Layer, usize)> {
let layer_selector_pane_idx: usize = ActivePane::LayerSelector.into();
let (layer_selector_pane, _) = &self.panes[layer_selector_pane_idx];
let selected_layer_idx = if let Some(Pane::LayerSelector(pane)) =
layer_selector_pane
{
pane.selected_layer_idx()
} else {
anyhow::bail!(
"layer selector pane is no longer at the expected position in the UI"
);
};
let (digest, layer) = self
.layers
.get_index(selected_layer_idx)
.context("selected layer has an invalid index")?;
Ok((digest, layer, selected_layer_idx))
}
pub fn get_aggregated_layers_changeset(
&self,
) -> anyhow::Result<(&LayerChangeSet, usize)> {
let layer_selector_pane_idx: usize = ActivePane::LayerSelector.into();
let (layer_selector_pane, _) = &self.panes[layer_selector_pane_idx];
if let Some(Pane::LayerSelector(pane)) = layer_selector_pane {
Ok(pane.aggregated_layers_changeset())
} else {
anyhow::bail!(
"layer selector pane is no longer at the expected position in the UI"
);
}
}
fn get_active_pane(&self) -> anyhow::Result<&Pane> {
self.panes
.get(Into::<usize>::into(self.active_pane))
.and_then(|(pane, _)| pane.as_ref())
.with_context(|| {
format!(
"bug: pane {:?} is no longer at its expected place",
self.active_pane
)
})
}
fn get_active_pane_mut(&mut self) -> anyhow::Result<&mut Pane> {
self.panes
.get_mut(Into::<usize>::into(self.active_pane))
.and_then(|(pane, _)| pane.as_mut())
.with_context(|| {
format!(
"bug: pane {:?} is no longer at its expected place",
self.active_pane
)
})
}
fn on_changeset_updated(&mut self) -> anyhow::Result<()> {
let layer_inspector_pane_idx: usize = ActivePane::LayerInspector.into();
let (layer_inspector_pane_opt, _) =
&mut self.panes[layer_inspector_pane_idx];
let mut layer_inspector_pane = layer_inspector_pane_opt.take();
if let Some(Pane::LayerInspector(pane)) = layer_inspector_pane.as_mut()
{
pane.reset();
let (changeset, _) = self.get_aggregated_layers_changeset()?;
let (_, _, current_layer_idx) = self.get_selected_layer()?;
pane.filter_current_changeset(changeset, current_layer_idx as u8);
} else {
anyhow::bail!(
"layer inspector pane is no longer at the expected position in the UI"
);
}
let (layer_inspector_pane_opt, _) =
&mut self.panes[layer_inspector_pane_idx];
layer_inspector_pane_opt
.replace(layer_inspector_pane.expect("unreacheable"));
Ok(())
}
fn apply_side_effect(
&mut self,
side_effect: SideEffect,
) -> anyhow::Result<()> {
match side_effect {
SideEffect::ChangesetUpdated | SideEffect::FiltersUpdated => {
self.on_changeset_updated()?
}
}
Ok(())
}
}
impl Store for AppState {
type Action = AppAction;
fn handle(&mut self, action: Self::Action) -> anyhow::Result<()> {
match action {
AppAction::Empty((width, height)) => {
tracing::trace!("Received an empty event");
let (pane_areas, command_bar) =
split_layout(Rect::new(0, 0, width, height));
debug_assert_eq!(
pane_areas.len(),
self.panes.len(),
"Each pane should have a corresponding rect that it will be rendered in"
);
pane_areas
.into_iter()
.zip(self.panes.iter_mut())
.for_each(|(new_area, (_, old_area))| *old_area = new_area);
self.command_bar_area = command_bar;
}
AppAction::TogglePane(direction) if !self.show_help_popup => {
self.active_pane.toggle(direction)
}
action @ (AppAction::Interact
| AppAction::Move(..)
| AppAction::Scroll(..)
| AppAction::Subaction)
if !self.show_help_popup =>
{
let active_pane_idx = Into::<usize>::into(self.active_pane);
let (active_pane, _) = &mut self.panes[active_pane_idx];
let mut active_pane =
active_pane.take().with_context(|| {
format!(
"bug: forgot to return the {active_pane_idx} pane?"
)
})?;
let side_effect: Option<SideEffect> = match action {
AppAction::Interact => {
active_pane.interact_within_pane(self).context(
"error while handling the 'interact' action",
)?;
None
}
AppAction::Move(direction) => active_pane
.move_within_pane(direction, self)
.context("error while handling the 'move' action")?,
AppAction::Scroll(direction) => {
active_pane
.scroll_within_pane(direction, self)
.context(
"error while handling the 'scroll' action",
)?;
None
}
AppAction::Subaction => active_pane.on_subaction(),
_ => unreachable!("Checked above"),
};
let (active_pane_opt, _) = &mut self.panes[active_pane_idx];
active_pane_opt.replace(active_pane);
if let Some(side_effect) = side_effect {
self.apply_side_effect(side_effect)
.context("error while applying a side effect")?
};
}
AppAction::Copy if !self.show_help_popup => {
if self.clipboard.is_none() {
tracing::trace!("Can't copy: no clipboard is available");
return Ok(());
}
let mut clipboard = self.clipboard.take();
if let Some(text_to_copy) =
self.get_active_pane()?.get_selected_field(self)
{
copy_to_clipboard(clipboard.as_mut(), text_to_copy);
}
self.clipboard = clipboard;
}
AppAction::ToggleHelpPane => {
self.show_help_popup = !self.show_help_popup;
}
AppAction::SelectPane(index) if !self.show_help_popup => self
.active_pane
.select(index)
.context("failed to select a pane by index")?,
AppAction::ToggleInputMode => {
let (is_in_insert_mode, side_effect) =
self.get_active_pane_mut()?.toggle_input_mode();
if let Some(side_effect) = side_effect {
self.apply_side_effect(side_effect)?;
}
self.is_in_insert_mode = is_in_insert_mode;
}
AppAction::InputCharacter(input) => {
self.get_active_pane_mut()?.on_input_character(input);
}
AppAction::InputDeleteCharacter => {
self.get_active_pane_mut()?.on_backspace();
}
_ => {}
};
Ok(())
}
}