use alloc::borrow::{Cow, ToOwned};
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cell::Cell;
use core::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, PoisonError, RwLock};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::{Modifier, Style, Styled};
use ratatui_core::text::{Line, Span, Text};
use ratatui_core::widgets::Widget;
use crate::native::store;
use crate::{
Catalog, MarkupKind, Message, NoErrors, Part, PartSink, StrRef, Tr, TrArgs, TrDyn, TrRich,
markup_key,
};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Markup {
key: u64,
name: &'static str,
}
impl Markup {
#[doc(hidden)]
#[must_use]
pub const fn new(key: u64, name: &'static str) -> Markup {
Markup { key, name }
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub const fn key(self) -> u64 {
self.key
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Theme {
entries: Vec<(Markup, Style)>,
}
impl Theme {
#[must_use]
pub const fn empty() -> Theme {
Theme {
entries: Vec::new(),
}
}
#[must_use]
pub fn style(mut self, markup: Markup, style: Style) -> Theme {
match self.entries.iter_mut().find(|(m, _)| m.key == markup.key) {
Some(entry) => *entry = (markup, style),
None => self.entries.push((markup, style)),
}
self
}
fn get(&self, key: u64) -> Option<Style> {
self.entries
.iter()
.find(|(m, _)| m.key == key)
.map(|(_, style)| *style)
}
}
impl Default for Theme {
fn default() -> Theme {
let named = |name: &'static str| Markup::new(markup_key(name), name);
let bold = Style::new().add_modifier(Modifier::BOLD);
let italic = Style::new().add_modifier(Modifier::ITALIC);
let crossed = Style::new().add_modifier(Modifier::CROSSED_OUT);
let reversed = Style::new().add_modifier(Modifier::REVERSED);
Theme::empty()
.style(named("b"), bold)
.style(named("strong"), bold)
.style(named("i"), italic)
.style(named("em"), italic)
.style(named("u"), Style::new().add_modifier(Modifier::UNDERLINED))
.style(named("s"), crossed)
.style(named("del"), crossed)
.style(named("code"), reversed)
.style(named("kbd"), reversed)
}
}
static THEME: LazyLock<RwLock<Arc<Theme>>> =
LazyLock::new(|| RwLock::new(Arc::new(Theme::default())));
static GENERATION: AtomicU64 = AtomicU64::new(1);
std::thread_local! {
static LOCAL: Cell<(u64, Option<Arc<Theme>>)> = const { Cell::new((0, None)) };
static SCOPED: Cell<Option<Arc<Theme>>> = const { Cell::new(None) };
}
pub fn set_theme(theme: Theme) {
let theme = Arc::new(theme);
let mut slot = THEME.write().unwrap_or_else(PoisonError::into_inner);
*slot = theme;
GENERATION.fetch_add(1, Ordering::Release);
}
pub fn with_theme<R>(theme: &Theme, body: impl FnOnce() -> R) -> R {
let outer = SCOPED.with(|scoped| scoped.replace(Some(Arc::new(theme.clone()))));
let _restore = Restore(outer);
body()
}
#[must_use]
pub fn theme() -> Theme {
Theme::clone(&in_force())
}
struct Restore(Option<Arc<Theme>>);
impl Drop for Restore {
fn drop(&mut self) {
let outer = self.0.take();
let _ = SCOPED.try_with(|scoped| scoped.set(outer));
}
}
fn in_force() -> Arc<Theme> {
let scoped = SCOPED.with(|scoped| {
let theme = scoped.take();
let copy = theme.clone();
scoped.set(theme);
copy
});
if let Some(theme) = scoped {
return theme;
}
let generation = GENERATION.load(Ordering::Acquire);
LOCAL.with(|local| {
let (taken, copy) = local.take();
let theme = match copy {
Some(theme) if taken == generation => theme,
_ => Arc::clone(&THEME.read().unwrap_or_else(PoisonError::into_inner)),
};
local.set((generation, Some(Arc::clone(&theme))));
theme
})
}
macro_rules! conversions {
($($ty:ty),*) => {$(
impl From<$ty> for Span<'static> {
fn from(description: $ty) -> Span<'static> {
span_of(&description)
}
}
impl From<&$ty> for Span<'static> {
fn from(description: &$ty) -> Span<'static> {
span_of(description)
}
}
impl From<$ty> for Line<'static> {
fn from(description: $ty) -> Line<'static> {
line_of(&description)
}
}
impl From<&$ty> for Line<'static> {
fn from(description: &$ty) -> Line<'static> {
line_of(description)
}
}
impl From<$ty> for Text<'static> {
fn from(description: $ty) -> Text<'static> {
text_of(&description)
}
}
impl From<&$ty> for Text<'static> {
fn from(description: &$ty) -> Text<'static> {
text_of(description)
}
}
impl Styled for $ty {
type Item = Line<'static>;
fn style(&self) -> Style {
Style::new()
}
fn set_style<S: Into<Style>>(self, style: S) -> Line<'static> {
line_of(&self).style(style)
}
}
impl Widget for $ty {
fn render(self, area: Rect, buf: &mut Buffer) {
text_of(&self).render(area, buf);
}
}
)*};
}
conversions!(Tr, TrArgs, TrRich, TrDyn);
fn span_of(m: &dyn Message) -> Span<'static> {
let content = store::with_formatter(|catalog, f| {
store::with_scratch(|buf| {
let mut flat = Flat {
catalog,
buf,
only: None,
};
m.parts(f, &mut flat, &mut NoErrors);
flat.finish()
})
});
match content {
Some(content) => Span::raw(content),
None => store::not_installed(),
}
}
fn line_of(m: &dyn Message) -> Line<'static> {
Line::from(lines_of(m, false).1)
}
fn text_of(m: &dyn Message) -> Text<'static> {
let (mut lines, last) = lines_of(m, true);
lines.push(Line::from(last));
Text::from(lines)
}
fn lines_of(m: &dyn Message, split: bool) -> (Vec<Line<'static>>, Vec<Span<'static>>) {
let out = store::with_formatter(|catalog, f| {
let mut sink = Draft {
catalog,
theme: None,
split,
open: [(0, Style::new()); DEPTH],
depth: 0,
spans: Vec::new(),
lines: Vec::new(),
};
m.parts(f, &mut sink, &mut NoErrors);
(sink.lines, sink.spans)
});
match out {
Some(out) => out,
None => store::not_installed(),
}
}
fn spaces(text: &mut String) {
while let Some(at) = text.find('\n') {
text.replace_range(at..=at, " ");
}
}
fn placeholder(write: impl FnOnce(&mut String)) -> String {
store::with_scratch(|buf| {
write(buf);
String::from(buf.as_str())
})
}
const DEPTH: usize = 8;
struct Draft {
catalog: &'static Catalog,
theme: Option<Arc<Theme>>,
split: bool,
open: [(u64, Style); DEPTH],
depth: usize,
spans: Vec<Span<'static>>,
lines: Vec<Line<'static>>,
}
impl Draft {
fn style(&self) -> Style {
self.depth
.min(DEPTH)
.checked_sub(1)
.and_then(|at| self.open.get(at))
.map_or_else(Style::new, |(_, style)| *style)
}
fn line_break(&mut self, style: Style) {
if self.split {
let spans = core::mem::take(&mut self.spans);
self.lines.push(Line::from(spans));
} else {
self.spans.push(Span::styled(" ", style));
}
}
fn borrowed(&mut self, text: &'static str) {
let style = self.style();
for (at, piece) in text.split('\n').enumerate() {
if at > 0 {
self.line_break(style);
}
if !piece.is_empty() {
self.spans.push(Span::styled(piece, style));
}
}
}
fn owned(&mut self, mut text: String) {
let style = self.style();
if !text.contains('\n') {
if !text.is_empty() {
self.spans.push(Span::styled(text, style));
}
} else if !self.split {
spaces(&mut text);
self.spans.push(Span::styled(text, style));
} else {
for (at, piece) in text.split('\n').enumerate() {
if at > 0 {
self.line_break(style);
}
if !piece.is_empty() {
self.spans.push(Span::styled(piece.to_owned(), style));
}
}
}
}
fn open(&mut self, key: u64) {
let outer = self.style();
let own = self.theme.get_or_insert_with(in_force).get(key);
if let Some(slot) = self.open.get_mut(self.depth) {
*slot = (key, own.map_or(outer, |own| outer.patch(own)));
}
self.depth = self.depth.saturating_add(1);
}
fn close(&mut self, key: u64) {
if self.depth > DEPTH {
self.depth -= 1;
} else if let Some(at) = self
.open
.get(..self.depth)
.and_then(|open| open.iter().rposition(|(k, _)| *k == key))
{
self.depth = at;
}
}
}
impl PartSink for Draft {
fn part(&mut self, part: Part<'_>) {
match part {
Part::Text(text) => self.owned(text.to_owned()),
Part::Expression(expression) => self.owned(placeholder(|buf| expression.write(buf))),
Part::Fallback(source) => self.owned(placeholder(|buf| {
buf.push('{');
source.write(buf);
buf.push('}');
})),
Part::Markup(markup) => match markup.kind() {
MarkupKind::Open => self.open(markup_key(markup.name())),
MarkupKind::Close => self.close(markup_key(markup.name())),
MarkupKind::Standalone => {}
},
_ => {}
}
}
fn part_catalog_text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
let own: &'static Catalog = self.catalog;
if core::ptr::eq(catalog, own) {
let Some(text) = own.text(r) else {
return false;
};
self.borrowed(text);
} else {
let Some(text) = catalog.text(r) else {
return false;
};
self.owned(text.to_owned());
}
true
}
}
struct Flat<'b> {
catalog: &'static Catalog,
buf: &'b mut String,
only: Option<&'static str>,
}
impl Flat<'_> {
fn spill(&mut self) {
if let Some(only) = self.only.take() {
self.buf.push_str(only);
}
}
fn borrowed(&mut self, text: &'static str) {
if text.is_empty() {
return;
}
if self.only.is_none() && self.buf.is_empty() && !text.contains('\n') {
self.only = Some(text);
} else {
self.spill();
self.buf.push_str(text);
}
}
fn finish(self) -> Cow<'static, str> {
match self.only {
Some(only) => Cow::Borrowed(only),
None if self.buf.is_empty() => Cow::Borrowed(""),
None => {
spaces(self.buf);
Cow::Owned(String::from(self.buf.as_str()))
}
}
}
}
impl PartSink for Flat<'_> {
fn part(&mut self, part: Part<'_>) {
match part {
Part::Text(text) => {
self.spill();
self.buf.push_str(text);
}
Part::Expression(expression) => {
self.spill();
expression.write(&mut *self.buf);
}
Part::Fallback(source) => {
self.spill();
self.buf.push('{');
source.write(&mut *self.buf);
self.buf.push('}');
}
_ => {}
}
}
fn part_catalog_text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
let own: &'static Catalog = self.catalog;
if core::ptr::eq(catalog, own) {
let Some(text) = own.text(r) else {
return false;
};
self.borrowed(text);
} else {
let Some(text) = catalog.text(r) else {
return false;
};
self.part(Part::Text(text));
}
true
}
}