use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use super::{Component, EventContext, RenderContext};
use crate::input::Event;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum DividerOrientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DividerMessage {
SetLabel(Option<String>),
SetOrientation(DividerOrientation),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct DividerState {
orientation: DividerOrientation,
label: Option<String>,
color: Option<Color>,
}
impl Default for DividerState {
fn default() -> Self {
Self {
orientation: DividerOrientation::Horizontal,
label: None,
color: None,
}
}
}
impl DividerState {
pub fn new() -> Self {
Self::default()
}
pub fn horizontal() -> Self {
Self {
orientation: DividerOrientation::Horizontal,
..Self::default()
}
}
pub fn vertical() -> Self {
Self {
orientation: DividerOrientation::Vertical,
..Self::default()
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn with_orientation(mut self, orientation: DividerOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn orientation(&self) -> &DividerOrientation {
&self.orientation
}
pub fn color(&self) -> Option<Color> {
self.color
}
pub fn set_label(&mut self, label: Option<String>) {
self.label = label;
}
pub fn set_color(&mut self, color: Option<Color>) {
self.color = color;
}
pub fn set_orientation(&mut self, orientation: DividerOrientation) {
self.orientation = orientation;
}
pub fn update(&mut self, msg: DividerMessage) -> Option<()> {
Divider::update(self, msg)
}
pub fn handle_event(&self, event: &Event) -> Option<DividerMessage> {
Divider::handle_event(self, event, &EventContext::default())
}
pub fn dispatch_event(&mut self, event: &Event) -> Option<()> {
Divider::dispatch_event(self, event, &EventContext::default())
}
}
pub struct Divider;
impl Component for Divider {
type State = DividerState;
type Message = DividerMessage;
type Output = ();
fn init() -> Self::State {
DividerState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
DividerMessage::SetLabel(label) => state.label = label,
DividerMessage::SetOrientation(orientation) => state.orientation = orientation,
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
crate::annotation::with_registry(|reg| {
reg.register(
ctx.area,
crate::annotation::Annotation::divider("divider")
.with_label(state.label.as_deref().unwrap_or(""))
.with_disabled(ctx.disabled),
);
});
if ctx.area.width == 0 || ctx.area.height == 0 {
return;
}
let style = if ctx.disabled {
ctx.theme.disabled_style()
} else if let Some(color) = state.color {
Style::default().fg(color)
} else {
ctx.theme.normal_style()
};
match state.orientation {
DividerOrientation::Horizontal => {
render_horizontal(state, ctx.frame, ctx.area, style);
}
DividerOrientation::Vertical => {
render_vertical(state, ctx.frame, ctx.area, style);
}
}
}
}
fn render_horizontal(state: &DividerState, frame: &mut Frame, area: Rect, style: Style) {
let line_char = "\u{2500}"; let width = area.width as usize;
let text = match &state.label {
Some(label) => {
let label_with_padding = format!(" {} ", label);
let label_len = label_with_padding.len();
if label_len >= width {
let truncated: String = label_with_padding.chars().take(width).collect();
Line::from(Span::styled(truncated, style))
} else {
let remaining = width - label_len;
let left = remaining / 2;
let right = remaining - left;
let line = format!(
"{}{}{}",
line_char.repeat(left),
label_with_padding,
line_char.repeat(right),
);
Line::from(Span::styled(line, style))
}
}
None => Line::from(Span::styled(line_char.repeat(width), style)),
};
let render_area = Rect::new(area.x, area.y, area.width, 1.min(area.height));
let paragraph = Paragraph::new(text);
frame.render_widget(paragraph, render_area);
}
fn render_vertical(state: &DividerState, frame: &mut Frame, area: Rect, style: Style) {
let line_char = "\u{2502}"; let height = area.height as usize;
let middle_row = height / 2;
let mut lines = Vec::with_capacity(height);
for row in 0..height {
let content = match &state.label {
Some(label) if row == middle_row => {
label
.chars()
.next()
.map_or_else(|| line_char.to_string(), |c| c.to_string())
}
_ => line_char.to_string(),
};
lines.push(Line::from(Span::styled(content, style)));
}
let render_area = Rect::new(area.x, area.y, 1.min(area.width), area.height);
let paragraph = Paragraph::new(lines);
frame.render_widget(paragraph, render_area);
}
#[cfg(test)]
mod tests;