use std::collections::HashMap;
pub mod priority {
pub const OVERLAY: u8 = 100;
pub const TEMPORARY: u8 = 75;
pub const PLUGIN: u8 = 50;
pub const NORMAL: u8 = 25;
pub const DEFAULT: u8 = 0;
}
#[derive(Clone, Debug, PartialEq)]
struct SlotEntry {
content: SlotContent,
priority: u8,
}
#[derive(Clone, Debug, Default)]
struct PrioritySlot {
entries: Vec<SlotEntry>,
}
impl PrioritySlot {
fn set(&mut self, content: SlotContent, priority: u8) {
self.entries.retain(|e| e.priority != priority);
self.entries.push(SlotEntry { content, priority });
self.entries.sort_by(|a, b| b.priority.cmp(&a.priority));
}
fn clear(&mut self, priority: u8) {
self.entries.retain(|e| e.priority != priority);
}
#[allow(dead_code)]
pub(crate) fn clear_all(&mut self) {
self.entries.clear();
}
fn get(&self) -> Option<&SlotContent> {
self.entries.first().map(|e| &e.content)
}
fn get_at_priority(&self, priority: u8) -> Option<&SlotContent> {
self.entries
.iter()
.find(|e| e.priority == priority)
.map(|e| &e.content)
}
fn is_empty(&self) -> bool {
self.entries.is_empty() || self.entries.iter().all(|e| e.content.is_empty())
}
fn current_priority(&self) -> Option<u8> {
self.entries.first().map(|e| e.priority)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SlotContent {
Text(String),
Styled { text: String, class: String },
}
impl SlotContent {
pub fn text(s: impl Into<String>) -> Self {
SlotContent::Text(s.into())
}
pub fn styled(text: impl Into<String>, class: impl Into<String>) -> Self {
SlotContent::Styled {
text: text.into(),
class: class.into(),
}
}
pub fn as_str(&self) -> &str {
match self {
SlotContent::Text(s) => s,
SlotContent::Styled { text, .. } => text,
}
}
pub fn style_class(&self) -> Option<&str> {
match self {
SlotContent::Styled { class, .. } => Some(class),
_ => None,
}
}
pub fn is_empty(&self) -> bool {
self.as_str().is_empty()
}
}
impl Default for SlotContent {
fn default() -> Self {
SlotContent::Text(String::new())
}
}
impl From<String> for SlotContent {
fn from(s: String) -> Self {
SlotContent::Text(s)
}
}
impl From<&str> for SlotContent {
fn from(s: &str) -> Self {
SlotContent::Text(s.to_string())
}
}
pub mod header_slots {
pub const LEFT: &str = "left";
pub const CENTER: &str = "center";
pub const RIGHT: &str = "right";
pub const TITLE: &str = "title";
}
pub mod status_slots {
pub const LEFT: &str = "left";
pub const CENTER: &str = "center";
pub const RIGHT: &str = "right";
pub const MODE: &str = "mode";
pub const MESSAGE: &str = "message";
pub const POSITION: &str = "position";
pub const COMMAND: &str = "command"; }
#[derive(Clone, Debug, Default)]
pub struct RegionSlots {
slots: HashMap<String, PrioritySlot>,
}
impl RegionSlots {
pub fn new() -> Self {
Self {
slots: HashMap::new(),
}
}
pub fn set(&mut self, slot: &str, content: impl Into<SlotContent>) {
self.set_at_priority(slot, content, priority::NORMAL);
}
pub fn set_at_priority(&mut self, slot: &str, content: impl Into<SlotContent>, prio: u8) {
self.slots
.entry(slot.to_string())
.or_default()
.set(content.into(), prio);
}
pub fn set_if_changed(&mut self, slot: &str, content: &str) -> bool {
self.set_if_changed_at_priority(slot, content, priority::NORMAL)
}
pub fn set_if_changed_at_priority(&mut self, slot: &str, content: &str, prio: u8) -> bool {
if let Some(ps) = self.slots.get(slot) {
if let Some(existing) = ps.get_at_priority(prio) {
if existing.as_str() == content {
return false; }
}
}
self.set_at_priority(slot, SlotContent::Text(content.to_string()), prio);
true
}
pub fn set_styled_if_changed(&mut self, slot: &str, text: &str, class: &str) -> bool {
self.set_styled_if_changed_at_priority(slot, text, class, priority::NORMAL)
}
pub fn set_styled_if_changed_at_priority(
&mut self,
slot: &str,
text: &str,
class: &str,
prio: u8,
) -> bool {
if let Some(ps) = self.slots.get(slot) {
if let Some(existing) = ps.get_at_priority(prio) {
if existing.as_str() == text && existing.style_class() == Some(class) {
return false; }
}
}
self.set_at_priority(
slot,
SlotContent::Styled {
text: text.to_string(),
class: class.to_string(),
},
prio,
);
true
}
pub fn clear(&mut self, slot: &str) {
self.clear_at_priority(slot, priority::NORMAL);
}
pub fn clear_at_priority(&mut self, slot: &str, prio: u8) {
if let Some(ps) = self.slots.get_mut(slot) {
ps.clear(prio);
if ps.is_empty() {
self.slots.remove(slot);
}
}
}
pub fn clear_all(&mut self, slot: &str) {
self.slots.remove(slot);
}
pub fn clear_if_set(&mut self, slot: &str) -> bool {
self.clear_if_set_at_priority(slot, priority::NORMAL)
}
pub fn clear_if_set_at_priority(&mut self, slot: &str, prio: u8) -> bool {
if let Some(ps) = self.slots.get_mut(slot) {
if ps.get_at_priority(prio).is_some() {
ps.clear(prio);
if ps.is_empty() {
self.slots.remove(slot);
}
return true;
}
}
false
}
pub fn get(&self, slot: &str) -> Option<&SlotContent> {
self.slots.get(slot).and_then(|ps| ps.get())
}
pub fn get_text(&self, slot: &str) -> &str {
self.slots
.get(slot)
.and_then(|ps| ps.get())
.map(|c| c.as_str())
.unwrap_or("")
}
pub fn has(&self, slot: &str) -> bool {
self.slots
.get(slot)
.map(|ps| !ps.is_empty())
.unwrap_or(false)
}
pub fn current_priority(&self, slot: &str) -> Option<u8> {
self.slots.get(slot).and_then(|ps| ps.current_priority())
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.slots.keys().map(|s| s.as_str())
}
}
#[derive(Clone, Debug, Default)]
pub struct Slots {
pub header: RegionSlots,
pub status: RegionSlots,
}
impl Slots {
pub fn new() -> Self {
Self {
header: RegionSlots::new(),
status: RegionSlots::new(),
}
}
}
pub trait UseSlots {
fn use_header_slot<'a>(&self, ctx: &'a crate::context::RenderContext, slot: &str) -> &'a str {
ctx.slots.header.get_text(slot)
}
fn use_status_slot<'a>(&self, ctx: &'a crate::context::RenderContext, slot: &str) -> &'a str {
ctx.slots.status.get_text(slot)
}
fn use_header_has(&self, ctx: &crate::context::RenderContext, slot: &str) -> bool {
ctx.slots.header.has(slot)
}
fn use_status_has(&self, ctx: &crate::context::RenderContext, slot: &str) -> bool {
ctx.slots.status.has(slot)
}
}
impl<T> UseSlots for T {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_region_slots() {
let mut region = RegionSlots::new();
region.set(status_slots::LEFT, "NORMAL");
assert_eq!(region.get_text(status_slots::LEFT), "NORMAL");
region.set(status_slots::LEFT, "OVERRIDE");
assert_eq!(region.get_text(status_slots::LEFT), "OVERRIDE");
assert!(region.has(status_slots::LEFT));
assert!(!region.has(status_slots::RIGHT));
}
#[test]
fn test_slot_clear() {
let mut region = RegionSlots::new();
region.set(status_slots::CENTER, "message");
assert!(region.has(status_slots::CENTER));
region.clear(status_slots::CENTER);
assert!(!region.has(status_slots::CENTER));
}
#[test]
fn test_slots_container() {
let mut slots = Slots::new();
slots.header.set(header_slots::TITLE, "My App");
slots.status.set(status_slots::MODE, "NORMAL");
assert_eq!(slots.header.get_text(header_slots::TITLE), "My App");
assert_eq!(slots.status.get_text(status_slots::MODE), "NORMAL");
assert!(!slots.header.has(status_slots::MODE));
assert!(!slots.status.has(header_slots::TITLE));
}
#[test]
fn test_styled_content() {
let mut region = RegionSlots::new();
region.set(
status_slots::MODE,
SlotContent::styled("INSERT", "mode_insert"),
);
let content = region.get(status_slots::MODE).unwrap();
assert_eq!(content.as_str(), "INSERT");
assert_eq!(content.style_class(), Some("mode_insert"));
}
#[test]
fn test_set_if_changed() {
let mut region = RegionSlots::new();
assert!(region.set_if_changed(status_slots::MODE, "NORMAL"));
assert_eq!(region.get_text(status_slots::MODE), "NORMAL");
assert!(!region.set_if_changed(status_slots::MODE, "NORMAL"));
assert!(region.set_if_changed(status_slots::MODE, "INSERT"));
assert_eq!(region.get_text(status_slots::MODE), "INSERT");
}
#[test]
fn test_set_styled_if_changed() {
let mut region = RegionSlots::new();
assert!(region.set_styled_if_changed(status_slots::MODE, "NORMAL", "mode_normal"));
assert!(!region.set_styled_if_changed(status_slots::MODE, "NORMAL", "mode_normal"));
assert!(region.set_styled_if_changed(status_slots::MODE, "INSERT", "mode_normal"));
assert!(region.set_styled_if_changed(status_slots::MODE, "INSERT", "mode_insert"));
}
#[test]
fn test_clear_if_set() {
let mut region = RegionSlots::new();
assert!(!region.clear_if_set(status_slots::MODE));
region.set(status_slots::MODE, "NORMAL");
assert!(region.clear_if_set(status_slots::MODE));
assert!(!region.clear_if_set(status_slots::MODE));
}
#[test]
fn test_priority_slot_basic() {
let mut ps = PrioritySlot::default();
ps.set(SlotContent::text("normal"), priority::NORMAL);
assert_eq!(ps.get().unwrap().as_str(), "normal");
assert_eq!(ps.current_priority(), Some(priority::NORMAL));
}
#[test]
fn test_priority_slot_layering() {
let mut ps = PrioritySlot::default();
ps.set(SlotContent::text("normal"), priority::NORMAL);
assert_eq!(ps.get().unwrap().as_str(), "normal");
ps.set(SlotContent::text("temporary"), priority::TEMPORARY);
assert_eq!(ps.get().unwrap().as_str(), "temporary");
assert_eq!(ps.current_priority(), Some(priority::TEMPORARY));
ps.set(SlotContent::text("overlay"), priority::OVERLAY);
assert_eq!(ps.get().unwrap().as_str(), "overlay");
ps.clear(priority::OVERLAY);
assert_eq!(ps.get().unwrap().as_str(), "temporary");
ps.clear(priority::TEMPORARY);
assert_eq!(ps.get().unwrap().as_str(), "normal");
ps.clear(priority::NORMAL);
assert!(ps.is_empty());
}
#[test]
fn test_region_slots_priority() {
let mut region = RegionSlots::new();
region.set(status_slots::MESSAGE, "Status: OK");
assert_eq!(region.get_text(status_slots::MESSAGE), "Status: OK");
region.set_at_priority(status_slots::MESSAGE, "File saved!", priority::TEMPORARY);
assert_eq!(region.get_text(status_slots::MESSAGE), "File saved!");
assert_eq!(
region.current_priority(status_slots::MESSAGE),
Some(priority::TEMPORARY)
);
region.clear_at_priority(status_slots::MESSAGE, priority::TEMPORARY);
assert_eq!(region.get_text(status_slots::MESSAGE), "Status: OK");
assert_eq!(
region.current_priority(status_slots::MESSAGE),
Some(priority::NORMAL)
);
}
#[test]
fn test_priority_set_if_changed() {
let mut region = RegionSlots::new();
assert!(region.set_if_changed_at_priority(
status_slots::MESSAGE,
"temp msg",
priority::TEMPORARY
));
assert!(!region.set_if_changed_at_priority(
status_slots::MESSAGE,
"temp msg",
priority::TEMPORARY
));
assert!(region.set_if_changed_at_priority(
status_slots::MESSAGE,
"temp msg",
priority::NORMAL
));
assert_eq!(region.get_text(status_slots::MESSAGE), "temp msg");
assert_eq!(
region.current_priority(status_slots::MESSAGE),
Some(priority::TEMPORARY)
);
}
#[test]
fn test_clear_all_priorities() {
let mut region = RegionSlots::new();
region.set_at_priority(status_slots::MESSAGE, "default", priority::DEFAULT);
region.set_at_priority(status_slots::MESSAGE, "normal", priority::NORMAL);
region.set_at_priority(status_slots::MESSAGE, "temp", priority::TEMPORARY);
assert_eq!(region.get_text(status_slots::MESSAGE), "temp");
region.clear_all(status_slots::MESSAGE);
assert!(!region.has(status_slots::MESSAGE));
assert_eq!(region.get_text(status_slots::MESSAGE), "");
}
}