1use crate::{use_head, MetaContext, ServerMetaContext};
2use leptos::{
3 attr::{any_attribute::AnyAttribute, Attribute},
4 component,
5 oco::Oco,
6 prelude::{ArcTrigger, Notify, Track},
7 reactive::{effect::RenderEffect, owner::use_context},
8 tachys::{
9 dom::document,
10 hydration::Cursor,
11 view::{
12 add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
13 RenderHtml,
14 },
15 },
16 text_prop::TextProp,
17 IntoView,
18};
19use or_poisoned::OrPoisoned;
20use std::sync::{
21 atomic::{AtomicU32, Ordering},
22 Arc, Mutex, RwLock,
23};
24
25#[derive(Clone, Default)]
27pub struct TitleContext {
28 id: Arc<AtomicU32>,
29 formatter_stack: Arc<RwLock<Vec<(TitleId, Formatter)>>>,
30 text_stack: Arc<RwLock<Vec<(TitleId, TextProp)>>>,
31 revalidate: ArcTrigger,
32 #[allow(clippy::type_complexity)]
33 effect: Arc<Mutex<Option<RenderEffect<Option<Oco<'static, str>>>>>>,
34}
35
36impl core::fmt::Debug for TitleContext {
37 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38 f.debug_tuple("TitleContext").finish()
39 }
40}
41
42type TitleId = u32;
43
44impl TitleContext {
45 fn next_id(&self) -> TitleId {
46 self.id.fetch_add(1, Ordering::Relaxed)
47 }
48
49 fn invalidate(&self) {
50 self.revalidate.notify();
51 }
52
53 fn spawn_effect(&self) {
54 let this = self.clone();
55 let revalidate = self.revalidate.clone();
56
57 let mut effect_lock = self.effect.lock().or_poisoned();
58 if effect_lock.is_none() {
59 *effect_lock = Some(RenderEffect::new({
60 move |_| {
61 revalidate.track();
62 let text = this.as_string();
63 document().set_title(text.as_deref().unwrap_or_default());
64 text
65 }
66 }));
67 }
68 }
69
70 fn push_text_and_formatter(
71 &self,
72 id: TitleId,
73 text: Option<TextProp>,
74 formatter: Option<Formatter>,
75 ) {
76 if let Some(text) = text {
77 self.text_stack.write().or_poisoned().push((id, text));
78 }
79 if let Some(formatter) = formatter {
80 self.formatter_stack
81 .write()
82 .or_poisoned()
83 .push((id, formatter));
84 }
85 self.invalidate();
86 }
87
88 fn update_text_and_formatter(
89 &self,
90 id: TitleId,
91 text: Option<TextProp>,
92 formatter: Option<Formatter>,
93 ) {
94 let mut text_stack = self.text_stack.write().or_poisoned();
95 let mut formatter_stack = self.formatter_stack.write().or_poisoned();
96 let text_pos =
97 text_stack.iter().position(|(item_id, _)| *item_id == id);
98 let formatter_pos = formatter_stack
99 .iter()
100 .position(|(item_id, _)| *item_id == id);
101
102 match (text_pos, text) {
103 (None, None) => {}
104 (Some(old), Some(new)) => {
105 text_stack[old].1 = new;
106 self.invalidate();
107 }
108 (Some(old), None) => {
109 text_stack.remove(old);
110 self.invalidate();
111 }
112 (None, Some(new)) => {
113 text_stack.push((id, new));
114 self.invalidate();
115 }
116 }
117 match (formatter_pos, formatter) {
118 (None, None) => {}
119 (Some(old), Some(new)) => {
120 formatter_stack[old].1 = new;
121 self.invalidate();
122 }
123 (Some(old), None) => {
124 formatter_stack.remove(old);
125 self.invalidate();
126 }
127 (None, Some(new)) => {
128 formatter_stack.push((id, new));
129 self.invalidate();
130 }
131 }
132 }
133
134 fn remove_id(&self, id: TitleId) -> (Option<TextProp>, Option<Formatter>) {
135 let mut text_stack = self.text_stack.write().or_poisoned();
136 let text = text_stack
137 .iter()
138 .position(|(item_id, _)| *item_id == id)
139 .map(|pos| text_stack.remove(pos).1);
140
141 let mut formatter_stack = self.formatter_stack.write().or_poisoned();
142 let formatter = formatter_stack
143 .iter()
144 .position(|(item_id, _)| *item_id == id)
145 .map(|pos| formatter_stack.remove(pos).1);
146
147 self.invalidate();
148
149 (text, formatter)
150 }
151
152 pub fn as_string(&self) -> Option<Oco<'static, str>> {
154 let title = self
155 .text_stack
156 .read()
157 .or_poisoned()
158 .last()
159 .map(|n| n.1.get());
160
161 title.map(|title| {
162 if let Some(formatter) =
163 self.formatter_stack.read().or_poisoned().last()
164 {
165 (formatter.1 .0)(title.into_owned()).into()
166 } else {
167 title
168 }
169 })
170 }
171}
172
173#[repr(transparent)]
175pub struct Formatter(Box<dyn Fn(String) -> String + Send + Sync>);
176
177impl<F> From<F> for Formatter
178where
179 F: Fn(String) -> String + Send + Sync + 'static,
180{
181 #[inline(always)]
182 fn from(f: F) -> Formatter {
183 Formatter(Box::new(f))
184 }
185}
186
187#[component]
228pub fn Title(
229 #[prop(optional, into)]
231 mut formatter: Option<Formatter>,
232 #[prop(optional, into)]
234 mut text: Option<TextProp>,
235) -> impl IntoView {
236 let meta = use_head();
237 let server_ctx = use_context::<ServerMetaContext>();
238 let id = meta.title.next_id();
239 if let Some(cx) = server_ctx {
240 cx.title
244 .push_text_and_formatter(id, text.take(), formatter.take());
245 };
246
247 TitleView {
248 id,
249 meta,
250 formatter,
251 text,
252 }
253}
254
255struct TitleView {
256 id: u32,
257 meta: MetaContext,
258 formatter: Option<Formatter>,
259 text: Option<TextProp>,
260}
261
262struct TitleViewState {
263 id: TitleId,
264 meta: MetaContext,
265 formatter: Option<Formatter>,
267 text: Option<TextProp>,
268}
269
270impl Drop for TitleViewState {
271 fn drop(&mut self) {
272 self.meta.title.remove_id(self.id);
275 }
276}
277
278impl Render for TitleView {
279 type State = TitleViewState;
280
281 fn build(self) -> Self::State {
282 let TitleView {
283 id,
284 meta,
285 formatter,
286 text,
287 } = self;
288 meta.title.spawn_effect();
289 TitleViewState {
290 id,
291 meta,
292 text,
293 formatter,
294 }
295 }
296
297 fn rebuild(self, _state: &mut Self::State) {
298 self.meta.title.update_text_and_formatter(
299 self.id,
300 self.text,
301 self.formatter,
302 );
303 }
304}
305
306impl AddAnyAttr for TitleView {
307 type Output<SomeNewAttr: Attribute> = TitleView;
308
309 fn add_any_attr<NewAttr: Attribute>(
310 self,
311 _attr: NewAttr,
312 ) -> Self::Output<NewAttr>
313 where
314 Self::Output<NewAttr>: RenderHtml,
315 {
316 self
317 }
318}
319
320impl RenderHtml for TitleView {
321 type AsyncOutput = Self;
322 type Owned = Self;
323
324 const MIN_LENGTH: usize = 0;
325
326 fn dry_resolve(&mut self) {}
327
328 async fn resolve(self) -> Self::AsyncOutput {
329 self
330 }
331
332 fn to_html_with_buf(
333 self,
334 _buf: &mut String,
335 _position: &mut Position,
336 _escape: bool,
337 _mark_branches: bool,
338 _extra_attrs: Vec<AnyAttribute>,
339 ) {
340 }
343
344 fn hydrate<const FROM_SERVER: bool>(
345 self,
346 _cursor: &Cursor,
347 _position: &PositionState,
348 ) -> Self::State {
349 let TitleView {
350 id,
351 meta,
352 formatter,
353 text,
354 } = self;
355 meta.title.spawn_effect();
356 meta.title.push_text_and_formatter(id, text, formatter);
358 TitleViewState {
359 id,
360 meta,
361 text: None,
362 formatter: None,
363 }
364 }
365
366 fn into_owned(self) -> Self::Owned {
367 self
368 }
369}
370
371impl Mountable for TitleViewState {
372 fn unmount(&mut self) {
373 let (text, formatter) = self.meta.title.remove_id(self.id);
374 if text.is_some() {
375 self.text = text;
376 }
377 if formatter.is_some() {
378 self.formatter = formatter;
379 }
380 }
381
382 fn mount(
383 &mut self,
384 _parent: &leptos::tachys::renderer::types::Element,
385 _marker: Option<&leptos::tachys::renderer::types::Node>,
386 ) {
387 self.meta.title.push_text_and_formatter(
393 self.id,
394 self.text.take(),
395 self.formatter.take(),
396 );
397 }
398
399 fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
400 false
401 }
402
403 fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
404 vec![]
405 }
406}