use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::widgets::{Block, Borders};
use crate::components::Component;
use crate::components::backlinks_panel::QueryPanel;
use crate::components::event_state::EventState;
use crate::components::events::{AppTx, InputEvent};
use crate::components::panel::PanelKind;
use crate::components::sidebar::SidebarComponent;
use crate::components::text_editor::TextEditorComponent;
use crate::settings::themes::Theme;
struct Slot {
kind: PanelKind,
visible: bool,
}
pub struct PanelOrder {
slots: Vec<Slot>,
focus: usize,
}
impl PanelOrder {
pub fn new() -> Self {
let slots = vec![
Slot {
kind: PanelKind::Sidebar,
visible: true,
},
Slot {
kind: PanelKind::Editor,
visible: true,
},
Slot {
kind: PanelKind::Query,
visible: false,
},
];
let focus = slots
.iter()
.position(|s| s.kind == PanelKind::Editor)
.expect("editor slot present");
Self { slots, focus }
}
pub fn focused(&self) -> PanelKind {
self.slots[self.focus].kind
}
pub fn prev_kind(&self) -> Option<PanelKind> {
self.focus.checked_sub(1).map(|i| self.slots[i].kind)
}
pub fn next_kind(&self) -> Option<PanelKind> {
self.slots.get(self.focus + 1).map(|s| s.kind)
}
pub fn focus(&mut self, kind: PanelKind) {
if let Some(i) = self.slots.iter().position(|s| s.kind == kind) {
self.focus = i;
}
}
pub fn is_visible(&self, kind: PanelKind) -> bool {
self.slots
.iter()
.find(|s| s.kind == kind)
.is_some_and(|s| s.visible)
}
pub fn show(&mut self, kind: PanelKind) {
if let Some(s) = self.slots.iter_mut().find(|s| s.kind == kind) {
s.visible = true;
}
}
pub fn hide(&mut self, kind: PanelKind) {
if kind == PanelKind::Editor {
return;
}
if let Some(s) = self.slots.iter_mut().find(|s| s.kind == kind) {
s.visible = false;
}
if !self.slots[self.focus].visible {
self.focus = self.nearest_visible(self.focus);
}
}
pub fn visible_in_order(&self) -> Vec<PanelKind> {
self.slots
.iter()
.filter(|s| s.visible)
.map(|s| s.kind)
.collect()
}
pub fn set_order(&mut self, order: &[PanelKind]) {
let focused_kind = self.focused();
let mut new: Vec<Slot> = Vec::with_capacity(self.slots.len());
for &k in order {
if let Some(pos) = self.slots.iter().position(|s| s.kind == k) {
new.push(self.slots.remove(pos));
}
}
new.append(&mut self.slots);
self.slots = new;
self.focus = self
.slots
.iter()
.position(|s| s.kind == focused_kind)
.unwrap_or(0);
}
fn nearest_visible(&self, from: usize) -> usize {
let n = self.slots.len();
(1..n)
.flat_map(|d| [from.checked_sub(d), Some(from + d).filter(|&i| i < n)])
.flatten()
.find(|&i| self.slots[i].visible)
.unwrap_or(from)
}
}
impl Default for PanelOrder {
fn default() -> Self {
Self::new()
}
}
fn panel_column(kind: PanelKind) -> Constraint {
match kind {
PanelKind::Sidebar => Constraint::Length(30),
PanelKind::Editor => Constraint::Min(0),
PanelKind::Query => Constraint::Length(40),
}
}
pub struct PanelSet {
order: PanelOrder,
sidebar: SidebarComponent,
editor: TextEditorComponent,
query: QueryPanel,
}
impl PanelSet {
pub fn from_panels(
sidebar: SidebarComponent,
editor: TextEditorComponent,
query: QueryPanel,
) -> Self {
Self {
order: PanelOrder::new(),
sidebar,
editor,
query,
}
}
pub fn focused(&self) -> PanelKind {
self.order.focused()
}
pub fn focused_label(&self) -> &'static str {
self.order.focused().label()
}
pub fn prev_kind(&self) -> Option<PanelKind> {
self.order.prev_kind()
}
pub fn next_kind(&self) -> Option<PanelKind> {
self.order.next_kind()
}
pub fn is_visible(&self, kind: PanelKind) -> bool {
self.order.is_visible(kind)
}
pub fn show(&mut self, kind: PanelKind) {
self.order.show(kind);
}
pub fn hide(&mut self, kind: PanelKind) {
self.order.hide(kind);
}
pub fn set_order(&mut self, order: &[PanelKind]) {
self.order.set_order(order);
}
pub fn focus(&mut self, kind: PanelKind) {
if kind != PanelKind::Editor {
self.editor.close_autocomplete();
}
self.order.focus(kind);
}
pub fn sidebar(&self) -> &SidebarComponent {
&self.sidebar
}
pub fn sidebar_mut(&mut self) -> &mut SidebarComponent {
&mut self.sidebar
}
pub fn editor(&self) -> &TextEditorComponent {
&self.editor
}
pub fn editor_mut(&mut self) -> &mut TextEditorComponent {
&mut self.editor
}
pub fn query(&self) -> &QueryPanel {
&self.query
}
pub fn query_mut(&mut self) -> &mut QueryPanel {
&mut self.query
}
pub fn focused_hints(&self) -> Vec<(String, String)> {
match self.order.focused() {
PanelKind::Sidebar => self.sidebar.hint_shortcuts(),
PanelKind::Editor => self.editor.hint_shortcuts(),
PanelKind::Query => self.query.hint_shortcuts(),
}
}
pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
match self.order.focused() {
PanelKind::Sidebar => self.sidebar.handle_input(event, tx),
PanelKind::Editor => self.editor.handle_input(event, tx),
PanelKind::Query => {
if let InputEvent::Key(key) = event {
self.query.handle_key(key, tx)
} else {
EventState::NotConsumed
}
}
}
}
pub fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme, show_focus: bool) {
let visible = self.order.visible_in_order();
if visible.is_empty() {
return;
}
let constraints: Vec<Constraint> = visible.iter().map(|k| panel_column(*k)).collect();
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(area);
let focused = self.order.focused();
for (i, kind) in visible.iter().enumerate() {
let is_focused = show_focus && *kind == focused;
let rect = columns[i];
match kind {
PanelKind::Sidebar => self.sidebar.render(f, rect, theme, is_focused),
PanelKind::Query => self.query.render(f, rect, theme, is_focused),
PanelKind::Editor => {
let title = if self.editor.is_dirty() {
"Editor [+]"
} else {
"Editor"
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(theme.border_style(is_focused))
.style(theme.base_style());
let inner = block.inner(rect);
f.render_widget(block, rect);
self.editor.render(f, inner, theme, is_focused);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_focus_is_editor() {
let order = PanelOrder::new();
assert_eq!(order.focused(), PanelKind::Editor);
}
#[test]
fn adjacent_kinds_follow_order_and_clamp_at_ends() {
let order = PanelOrder::new();
assert_eq!(order.prev_kind(), Some(PanelKind::Sidebar));
assert_eq!(order.next_kind(), Some(PanelKind::Query));
}
#[test]
fn focus_moves_and_clamps_at_ends() {
let mut order = PanelOrder::new();
order.focus(PanelKind::Sidebar);
assert_eq!(order.focused(), PanelKind::Sidebar);
assert_eq!(order.prev_kind(), None);
assert_eq!(order.next_kind(), Some(PanelKind::Editor));
order.focus(PanelKind::Query);
assert_eq!(order.prev_kind(), Some(PanelKind::Editor));
assert_eq!(order.next_kind(), None);
}
#[test]
fn show_hide_toggles_visibility_except_editor() {
let mut order = PanelOrder::new();
assert!(order.is_visible(PanelKind::Sidebar));
assert!(!order.is_visible(PanelKind::Query));
order.show(PanelKind::Query);
assert!(order.is_visible(PanelKind::Query));
order.hide(PanelKind::Sidebar);
assert!(!order.is_visible(PanelKind::Sidebar));
order.hide(PanelKind::Editor);
assert!(order.is_visible(PanelKind::Editor));
}
#[test]
fn hiding_focused_panel_moves_focus_to_visible() {
let mut order = PanelOrder::new();
order.focus(PanelKind::Sidebar);
order.hide(PanelKind::Sidebar);
assert!(order.is_visible(order.focused()));
assert_eq!(order.focused(), PanelKind::Editor);
}
#[test]
fn set_order_permutes_keeping_focus_and_visibility() {
let mut order = PanelOrder::new();
order.set_order(&[PanelKind::Query, PanelKind::Editor, PanelKind::Sidebar]);
assert_eq!(order.focused(), PanelKind::Editor);
assert_eq!(order.prev_kind(), Some(PanelKind::Query));
assert_eq!(order.next_kind(), Some(PanelKind::Sidebar));
assert!(order.is_visible(PanelKind::Sidebar));
assert!(!order.is_visible(PanelKind::Query));
}
#[test]
fn visible_in_order_skips_hidden_and_follows_order() {
let mut order = PanelOrder::new();
assert_eq!(
order.visible_in_order(),
vec![PanelKind::Sidebar, PanelKind::Editor]
);
order.show(PanelKind::Query);
order.set_order(&[PanelKind::Query, PanelKind::Editor, PanelKind::Sidebar]);
assert_eq!(
order.visible_in_order(),
vec![PanelKind::Query, PanelKind::Editor, PanelKind::Sidebar]
);
}
}