use crate::{use_head, MetaContext, ServerMetaContext};
use leptos::{
attr::{any_attribute::AnyAttribute, Attribute},
component,
oco::Oco,
prelude::{ArcTrigger, Notify, Track},
reactive::{effect::RenderEffect, owner::use_context},
tachys::{
dom::document,
hydration::Cursor,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
},
text_prop::TextProp,
IntoView,
};
use or_poisoned::OrPoisoned;
use std::sync::{
atomic::{AtomicU32, Ordering},
Arc, Mutex, RwLock,
};
#[derive(Clone, Default)]
pub struct TitleContext {
id: Arc<AtomicU32>,
formatter_stack: Arc<RwLock<Vec<(TitleId, Formatter)>>>,
text_stack: Arc<RwLock<Vec<(TitleId, TextProp)>>>,
revalidate: ArcTrigger,
#[allow(clippy::type_complexity)]
effect: Arc<Mutex<Option<RenderEffect<Option<Oco<'static, str>>>>>>,
}
impl core::fmt::Debug for TitleContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
type TitleId = u32;
impl TitleContext {
fn next_id(&self) -> TitleId {
self.id.fetch_add(1, Ordering::Relaxed)
}
fn invalidate(&self) {
self.revalidate.notify();
}
fn spawn_effect(&self) {
let this = self.clone();
let revalidate = self.revalidate.clone();
let mut effect_lock = self.effect.lock().or_poisoned();
if effect_lock.is_none() {
*effect_lock = Some(RenderEffect::new({
move |_| {
revalidate.track();
let text = this.as_string();
document().set_title(text.as_deref().unwrap_or_default());
text
}
}));
}
}
fn push_text_and_formatter(
&self,
id: TitleId,
text: Option<TextProp>,
formatter: Option<Formatter>,
) {
if let Some(text) = text {
self.text_stack.write().or_poisoned().push((id, text));
}
if let Some(formatter) = formatter {
self.formatter_stack
.write()
.or_poisoned()
.push((id, formatter));
}
self.invalidate();
}
fn update_text_and_formatter(
&self,
id: TitleId,
text: Option<TextProp>,
formatter: Option<Formatter>,
) {
let mut text_stack = self.text_stack.write().or_poisoned();
let mut formatter_stack = self.formatter_stack.write().or_poisoned();
let text_pos =
text_stack.iter().position(|(item_id, _)| *item_id == id);
let formatter_pos = formatter_stack
.iter()
.position(|(item_id, _)| *item_id == id);
match (text_pos, text) {
(None, None) => {}
(Some(old), Some(new)) => {
text_stack[old].1 = new;
self.invalidate();
}
(Some(old), None) => {
text_stack.remove(old);
self.invalidate();
}
(None, Some(new)) => {
text_stack.push((id, new));
self.invalidate();
}
}
match (formatter_pos, formatter) {
(None, None) => {}
(Some(old), Some(new)) => {
formatter_stack[old].1 = new;
self.invalidate();
}
(Some(old), None) => {
formatter_stack.remove(old);
self.invalidate();
}
(None, Some(new)) => {
formatter_stack.push((id, new));
self.invalidate();
}
}
}
fn remove_id(&self, id: TitleId) -> (Option<TextProp>, Option<Formatter>) {
let mut text_stack = self.text_stack.write().or_poisoned();
let text = text_stack
.iter()
.position(|(item_id, _)| *item_id == id)
.map(|pos| text_stack.remove(pos).1);
let mut formatter_stack = self.formatter_stack.write().or_poisoned();
let formatter = formatter_stack
.iter()
.position(|(item_id, _)| *item_id == id)
.map(|pos| formatter_stack.remove(pos).1);
self.invalidate();
(text, formatter)
}
pub fn as_string(&self) -> Option<Oco<'static, str>> {
let title = self
.text_stack
.read()
.or_poisoned()
.last()
.map(|n| n.1.get());
title.map(|title| {
if let Some(formatter) =
self.formatter_stack.read().or_poisoned().last()
{
(formatter.1 .0)(title.into_owned()).into()
} else {
title
}
})
}
}
#[repr(transparent)]
pub struct Formatter(Box<dyn Fn(String) -> String + Send + Sync>);
impl<F> From<F> for Formatter
where
F: Fn(String) -> String + Send + Sync + 'static,
{
#[inline(always)]
fn from(f: F) -> Formatter {
Formatter(Box::new(f))
}
}
#[component]
pub fn Title(
#[prop(optional, into)]
mut formatter: Option<Formatter>,
#[prop(optional, into)]
mut text: Option<TextProp>,
) -> impl IntoView {
let meta = use_head();
let server_ctx = use_context::<ServerMetaContext>();
let id = meta.title.next_id();
if let Some(cx) = server_ctx {
cx.title
.push_text_and_formatter(id, text.take(), formatter.take());
};
TitleView {
id,
meta,
formatter,
text,
}
}
struct TitleView {
id: u32,
meta: MetaContext,
formatter: Option<Formatter>,
text: Option<TextProp>,
}
struct TitleViewState {
id: TitleId,
meta: MetaContext,
formatter: Option<Formatter>,
text: Option<TextProp>,
}
impl Drop for TitleViewState {
fn drop(&mut self) {
self.meta.title.remove_id(self.id);
}
}
impl Render for TitleView {
type State = TitleViewState;
fn build(self) -> Self::State {
let TitleView {
id,
meta,
formatter,
text,
} = self;
meta.title.spawn_effect();
TitleViewState {
id,
meta,
text,
formatter,
}
}
fn rebuild(self, _state: &mut Self::State) {
self.meta.title.update_text_and_formatter(
self.id,
self.text,
self.formatter,
);
}
}
impl AddAnyAttr for TitleView {
type Output<SomeNewAttr: Attribute> = TitleView;
fn add_any_attr<NewAttr: Attribute>(
self,
_attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
self
}
}
impl RenderHtml for TitleView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
const EXISTS: bool = false;
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn to_html_with_buf(
self,
_buf: &mut String,
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
}
fn hydrate<const FROM_SERVER: bool>(
self,
_cursor: &Cursor,
_position: &PositionState,
) -> Self::State {
let TitleView {
id,
meta,
formatter,
text,
} = self;
meta.title.spawn_effect();
meta.title.push_text_and_formatter(id, text, formatter);
TitleViewState {
id,
meta,
text: None,
formatter: None,
}
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl Mountable for TitleViewState {
fn unmount(&mut self) {
let (text, formatter) = self.meta.title.remove_id(self.id);
if text.is_some() {
self.text = text;
}
if formatter.is_some() {
self.formatter = formatter;
}
}
fn mount(
&mut self,
_parent: &leptos::tachys::renderer::types::Element,
_marker: Option<&leptos::tachys::renderer::types::Node>,
) {
self.meta.title.push_text_and_formatter(
self.id,
self.text.take(),
self.formatter.take(),
);
}
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![]
}
}