use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use rich::cells::cell_len;
use rich::measure::Measurement;
use rich::style::{Meta, MetaValue};
use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
use crate::model::MicroAsset;
use crate::registry::MicroRegistry;
use crate::rows::runs;
pub const MICRO_META_KEY: &str = "rich.micro";
pub const PAD_CELL: char = '\u{2800}';
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
pub(crate) const SENTINEL_BASE: u32 = 0x10_0000;
pub(crate) const SENTINEL_LAST: u32 = 0x10_FFFD;
pub(crate) fn is_sentinel(c: char) -> bool {
(SENTINEL_BASE..=SENTINEL_LAST).contains(&(c as u32))
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct MicroMeta {
pub name: String,
pub cols: usize,
pub rows: usize,
pub id: u64,
pub row: usize,
}
impl MicroMeta {
pub fn new(asset: &MicroAsset) -> Self {
MicroMeta {
name: asset.name().to_string(),
cols: asset.cols(),
rows: asset.size().rows(),
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
row: 0,
}
}
#[must_use]
pub fn with_row(&self, row: usize) -> Self {
MicroMeta {
row,
..self.clone()
}
}
pub fn to_value(&self) -> MetaValue {
let mut items = vec![
MetaValue::Str(self.name.clone()),
MetaValue::Int(self.cols as i64),
MetaValue::Int(self.rows as i64),
MetaValue::Int(self.id as i64),
];
if self.row > 0 {
items.push(MetaValue::Int(self.row as i64));
}
MetaValue::List(items)
}
pub fn to_style(&self) -> Style {
let mut meta = Meta::new();
meta.insert(MICRO_META_KEY, self.to_value());
Style::from_meta(meta)
}
pub fn from_style(style: &Style) -> Option<Self> {
let MetaValue::List(items) = style.meta_ref()?.get(MICRO_META_KEY)? else {
return None;
};
let (head, row) = match items.as_slice() {
[head @ .., MetaValue::Int(row)] if items.len() == 5 => (head, *row),
head if items.len() == 4 => (head, 0),
_ => return None,
};
match head {
[
MetaValue::Str(name),
MetaValue::Int(cols),
MetaValue::Int(rows),
MetaValue::Int(id),
] if *cols > 0 && *rows > 0 && (0..*rows).contains(&row) => Some(MicroMeta {
name: name.clone(),
cols: *cols as usize,
rows: *rows as usize,
id: *id as u64,
row: row as usize,
}),
_ => None,
}
}
pub fn from_segment(segment: &Segment) -> Option<Self> {
if segment.control {
return None;
}
segment.style.as_ref().and_then(Self::from_style)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FallbackPreference {
#[default]
Emoji,
Text,
Alt,
}
fn pad(value: &str, cols: usize) -> String {
let width = cell_len(value);
let mut out = value.to_string();
out.extend(std::iter::repeat_n(PAD_CELL, cols.saturating_sub(width)));
out
}
fn alt_cells(alt: &str, cols: usize) -> String {
let mut out = String::new();
let mut width = 0;
for c in alt.chars() {
let c = if c.is_whitespace() || c.is_control() || is_sentinel(c) {
PAD_CELL
} else {
c
};
let w = cell_len(c.encode_utf8(&mut [0; 4]));
if width + w > cols {
break;
}
out.push(c);
width += w;
}
pad(&out, cols)
}
pub fn fallback_cells(asset: &MicroAsset, preference: FallbackPreference) -> String {
let cols = asset.cols();
let fallback = asset.fallback();
let choice = match preference {
FallbackPreference::Emoji => fallback.emoji.as_ref().or(fallback.text.as_ref()),
FallbackPreference::Text => fallback.text.as_ref(),
FallbackPreference::Alt => None,
};
match choice {
Some(value) if cell_len(value) <= cols && cell_len(value) > 0 => pad(value, cols),
_ => alt_cells(asset.alt(), cols),
}
}
pub fn fallback_row(asset: &MicroAsset, preference: FallbackPreference, row: usize) -> String {
if row == 0 {
fallback_cells(asset, preference)
} else {
PAD_CELL.to_string().repeat(asset.cols())
}
}
pub fn placeholder(asset: &MicroAsset, preference: FallbackPreference) -> Text {
placeholder_with(asset, preference, MicroMeta::new(asset))
}
pub(crate) fn placeholder_with(
asset: &MicroAsset,
preference: FallbackPreference,
meta: MicroMeta,
) -> Text {
let cells = fallback_cells(asset, preference);
let mut text = Text::new("");
text.append(&cells, Some(meta.to_style().into()));
text
}
#[derive(Clone, Debug)]
pub struct Placement<'a> {
pub asset: &'a MicroAsset,
pub meta: MicroMeta,
pub style: Option<Style>,
pub fallback: String,
}
pub trait MicroRenderer: Send + Sync {
fn name(&self) -> &str;
fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>>;
}
impl<T: MicroRenderer + ?Sized> MicroRenderer for Arc<T> {
fn name(&self) -> &str {
(**self).name()
}
fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>> {
(**self).render(placement, console)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FallbackRenderer {
pub preference: FallbackPreference,
}
impl FallbackRenderer {
pub fn new(preference: FallbackPreference) -> Self {
FallbackRenderer { preference }
}
}
impl MicroRenderer for FallbackRenderer {
fn name(&self) -> &str {
"fallback"
}
fn render(&self, placement: &Placement<'_>, _console: &Console) -> Option<Vec<Segment>> {
Some(vec![Segment::new(
fallback_row(placement.asset, self.preference, placement.meta.row),
placement.style.clone(),
)])
}
}
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct MicroView<R> {
inner: R,
registry: Arc<MicroRegistry>,
renderers: Vec<Arc<dyn MicroRenderer>>,
}
impl<R: Renderable> MicroView<R> {
pub fn new(inner: R, registry: Arc<MicroRegistry>) -> Self {
MicroView {
inner,
registry,
renderers: Vec::new(),
}
}
pub fn renderer(mut self, renderer: Arc<dyn MicroRenderer>) -> Self {
self.renderers.push(renderer);
self
}
}
impl<R: Renderable> Renderable for MicroView<R> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let segments = self.inner.rich_render(console, options);
if self.renderers.is_empty() {
return segments;
}
rewrite(segments, &self.registry, &self.renderers, console)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.inner.measure(console, options)
}
}
pub fn rewrite(
segments: Vec<Segment>,
registry: &MicroRegistry,
renderers: &[Arc<dyn MicroRenderer>],
console: &Console,
) -> Vec<Segment> {
rewrite_rows(segments, registry, renderers, console, false)
}
fn width_of(drawn: &[Segment]) -> usize {
drawn.iter().map(Segment::cell_length).sum()
}
pub(crate) fn rewrite_rows(
segments: Vec<Segment>,
registry: &MicroRegistry,
renderers: &[Arc<dyn MicroRenderer>],
console: &Console,
each_row: bool,
) -> Vec<Segment> {
let found = runs(&segments);
if found.is_empty() {
return segments;
}
let mut by_row: HashMap<(u64, usize), Vec<usize>> = HashMap::new();
for (index, run) in found.iter().enumerate() {
by_row
.entry((run.meta.id, run.meta.row))
.or_default()
.push(index);
}
let placement = |index: usize, asset| {
let run = &found[index];
Placement {
asset,
meta: run.meta.clone(),
style: segments[run.start].style.clone(),
fallback: segments[run.start..run.end]
.iter()
.map(|s| s.text.as_str())
.collect(),
}
};
let mut drawn: Vec<Option<Vec<Segment>>> = vec![None; found.len()];
for (index, run) in found.iter().enumerate() {
let meta = &run.meta;
let asset = registry
.resolve(&meta.name)
.filter(|asset| asset.cols() == meta.cols && asset.rows() == meta.rows);
let Some(asset) = asset.filter(|_| run.width == meta.cols) else {
continue;
};
if meta.rows == 1 || each_row {
let placement = placement(index, asset);
drawn[index] = renderers.iter().find_map(|renderer| {
renderer
.render(&placement, console)
.filter(|cells| width_of(cells) == meta.cols)
});
continue;
}
if meta.row != 0 {
continue;
}
let mut group = vec![index];
for row in 1..meta.rows {
let below = by_row.get(&(meta.id, row)).and_then(|candidates| {
candidates.iter().copied().find(|&other| {
let other = &found[other];
other.line == run.line + row
&& other.column == run.column
&& other.width == meta.cols
})
});
match below {
Some(other) => group.push(other),
None => break,
}
}
if group.len() != meta.rows {
continue;
}
let placements: Vec<Placement<'_>> = group.iter().map(|&i| placement(i, asset)).collect();
let rows = renderers.iter().find_map(|renderer| {
placements
.iter()
.map(|placement| {
renderer
.render(placement, console)
.filter(|cells| width_of(cells) == meta.cols)
})
.collect::<Option<Vec<_>>>()
});
if let Some(rows) = rows {
for (&other, cells) in group.iter().zip(rows) {
drawn[other] = Some(cells);
}
}
}
let mut out = Vec::with_capacity(segments.len());
let mut rest = segments.into_iter();
let mut at = 0;
for (run, cells) in found.iter().zip(drawn) {
out.extend(rest.by_ref().take(run.start - at));
let original: Vec<Segment> = rest.by_ref().take(run.end - run.start).collect();
match cells {
Some(cells) => out.extend(cells),
None => out.extend(original),
}
at = run.end;
}
out.extend(rest);
out
}