use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use super::{Component, RenderContext};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum KeyHintsLayout {
#[default]
Spaced,
Inline,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct KeyHint {
key: String,
action: String,
enabled: bool,
priority: u8,
}
impl KeyHint {
pub fn new(key: impl Into<String>, action: impl Into<String>) -> Self {
Self {
key: key.into(),
action: action.into(),
enabled: true,
priority: 100,
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn key(&self) -> &str {
&self.key
}
pub fn action(&self) -> &str {
&self.action
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn priority(&self) -> u8 {
self.priority
}
pub fn set_key(&mut self, key: impl Into<String>) {
self.key = key.into();
}
pub fn set_action(&mut self, action: impl Into<String>) {
self.action = action.into();
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn set_priority(&mut self, priority: u8) {
self.priority = priority;
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum KeyHintsMessage {
SetHints(Vec<KeyHint>),
AddHint(KeyHint),
RemoveHint(String),
EnableHint(String),
DisableHint(String),
SetLayout(KeyHintsLayout),
Clear,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct KeyHintsState {
hints: Vec<KeyHint>,
layout: KeyHintsLayout,
key_action_separator: String,
hint_separator: String,
key_style: Style,
action_style: Style,
disabled: bool,
}
impl Default for KeyHintsState {
fn default() -> Self {
Self {
hints: Vec::new(),
layout: KeyHintsLayout::default(),
key_action_separator: " ".to_string(),
hint_separator: " ".to_string(),
key_style: Style::default().fg(Color::Green),
action_style: Style::default(),
disabled: false,
}
}
}
impl KeyHintsState {
pub fn new() -> Self {
Self::default()
}
pub fn with_hints(hints: Vec<KeyHint>) -> Self {
Self {
hints,
..Self::default()
}
}
pub fn with_layout(mut self, layout: KeyHintsLayout) -> Self {
self.layout = layout;
self
}
pub fn with_key_action_separator(mut self, sep: impl Into<String>) -> Self {
self.key_action_separator = sep.into();
self
}
pub fn with_hint_separator(mut self, sep: impl Into<String>) -> Self {
self.hint_separator = sep.into();
self
}
pub fn with_key_style(mut self, style: Style) -> Self {
self.key_style = style;
self
}
pub fn with_action_style(mut self, style: Style) -> Self {
self.action_style = style;
self
}
pub fn hint(mut self, key: impl Into<String>, action: impl Into<String>) -> Self {
self.hints.push(KeyHint::new(key, action));
self
}
pub fn hint_with_priority(
mut self,
key: impl Into<String>,
action: impl Into<String>,
priority: u8,
) -> Self {
self.hints
.push(KeyHint::new(key, action).with_priority(priority));
self
}
pub fn hints(&self) -> &[KeyHint] {
&self.hints
}
pub fn visible_hints(&self) -> Vec<&KeyHint> {
let mut visible: Vec<_> = self.hints.iter().filter(|h| h.enabled).collect();
visible.sort_by_key(|h| h.priority);
visible
}
pub fn layout(&self) -> KeyHintsLayout {
self.layout
}
pub fn len(&self) -> usize {
self.hints.len()
}
pub fn is_empty(&self) -> bool {
self.hints.is_empty()
}
pub fn set_hints(&mut self, hints: Vec<KeyHint>) {
self.hints = hints;
}
pub fn add_hint(&mut self, hint: KeyHint) {
self.hints.push(hint);
}
pub fn remove_hint(&mut self, key: &str) {
self.hints.retain(|h| h.key != key);
}
pub fn enable_hint(&mut self, key: &str) {
if let Some(hint) = self.hints.iter_mut().find(|h| h.key == key) {
hint.enabled = true;
}
}
pub fn disable_hint(&mut self, key: &str) {
if let Some(hint) = self.hints.iter_mut().find(|h| h.key == key) {
hint.enabled = false;
}
}
pub fn set_layout(&mut self, layout: KeyHintsLayout) {
self.layout = layout;
}
pub fn clear(&mut self) {
self.hints.clear();
}
pub fn key_style(&self) -> Style {
self.key_style
}
pub fn action_style(&self) -> Style {
self.action_style
}
pub fn set_key_style(&mut self, style: Style) {
self.key_style = style;
}
pub fn set_action_style(&mut self, style: Style) {
self.action_style = style;
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = disabled;
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
pub struct KeyHints;
impl Component for KeyHints {
type State = KeyHintsState;
type Message = KeyHintsMessage;
type Output = ();
fn init() -> Self::State {
KeyHintsState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
KeyHintsMessage::SetHints(hints) => {
state.hints = hints;
}
KeyHintsMessage::AddHint(hint) => {
state.hints.push(hint);
}
KeyHintsMessage::RemoveHint(key) => {
state.hints.retain(|h| h.key != key);
}
KeyHintsMessage::EnableHint(key) => {
if let Some(hint) = state.hints.iter_mut().find(|h| h.key == key) {
hint.enabled = true;
}
}
KeyHintsMessage::DisableHint(key) => {
if let Some(hint) = state.hints.iter_mut().find(|h| h.key == key) {
hint.enabled = false;
}
}
KeyHintsMessage::SetLayout(layout) => {
state.layout = layout;
}
KeyHintsMessage::Clear => {
state.hints.clear();
}
}
None }
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
if state.hints.is_empty() || ctx.area.width == 0 || ctx.area.height == 0 {
return;
}
let visible = state.visible_hints();
if visible.is_empty() {
return;
}
let mut spans = Vec::new();
let key_style = if state.key_style == Style::default().fg(Color::Green) {
ctx.theme.success_style()
} else {
state.key_style
};
for (i, hint) in visible.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(&state.hint_separator));
}
spans.push(Span::styled(&hint.key, key_style));
spans.push(Span::raw(&state.key_action_separator));
spans.push(Span::styled(&hint.action, state.action_style));
}
let line = Line::from(spans);
let alignment = match state.layout {
KeyHintsLayout::Spaced => Alignment::Center,
KeyHintsLayout::Inline => Alignment::Left,
};
let paragraph = Paragraph::new(line).alignment(alignment);
let annotation =
crate::annotation::Annotation::new(crate::annotation::WidgetType::KeyHints)
.with_id("key_hints");
let annotated = crate::annotation::Annotate::new(paragraph, annotation);
ctx.frame.render_widget(annotated, ctx.area);
}
}
#[cfg(test)]
mod tests;