use crate::{SelectableText, SelectableTextOptions, SelectableTextWrap};
use gpui::{
App, Component, ElementId, IntoElement, RenderOnce, SharedString, TextStyle, Window,
prelude::*, px,
};
use liora_core::{Config, ui_font_family};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
panic::Location,
};
pub struct Title {
content: SharedString,
level: u8,
selectable: bool,
id: SharedString,
}
fn default_title_id(seed: &str, location: &Location<'_>) -> SharedString {
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
format!(
"title-{}:{}:{}-{:016x}",
location.file(),
location.line(),
location.column(),
hasher.finish()
)
.into()
}
impl Title {
#[track_caller]
pub fn new(content: impl Into<SharedString>) -> Self {
let content = content.into();
let id = default_title_id(content.as_ref(), Location::caller());
Self {
content,
level: 1,
selectable: true,
id,
}
}
pub fn h1(mut self) -> Self {
self.level = 1;
self
}
pub fn h2(mut self) -> Self {
self.level = 2;
self
}
pub fn h3(mut self) -> Self {
self.level = 3;
self
}
pub fn h4(mut self) -> Self {
self.level = 4;
self
}
pub fn h5(mut self) -> Self {
self.level = 5;
self
}
pub fn h6(mut self) -> Self {
self.level = 6;
self
}
pub fn selectable(mut self, selectable: bool) -> Self {
self.selectable = selectable;
self
}
pub fn id(mut self, id: impl Into<SharedString>) -> Self {
self.id = id.into();
self
}
pub fn register_key_bindings(cx: &mut App) {
SelectableText::register_key_bindings(cx);
}
fn render_with_theme(
self,
theme: &liora_theme::Theme,
window: &mut Window,
cx: &mut App,
) -> gpui::AnyElement {
let (size, weight) = match self.level {
1 => (theme.font_size.xl + 4.0, gpui::FontWeight::BOLD),
2 => (theme.font_size.xl, gpui::FontWeight::BOLD),
3 => (theme.font_size.lg + 2.0, gpui::FontWeight::BOLD),
4 => (theme.font_size.lg, gpui::FontWeight::BOLD),
5 => (theme.font_size.md, gpui::FontWeight::BOLD),
_ => (theme.font_size.sm, gpui::FontWeight::BOLD),
};
let font_size = px(size);
let line_height = font_size * 1.35;
let text_color = theme.neutral.text_1;
let ui_family = ui_font_family(cx);
if self.selectable {
let mut style = TextStyle::default();
style.color = text_color;
style.font_size = font_size.into();
style.line_height = line_height.into();
style.font_weight = weight;
style.white_space = gpui::WhiteSpace::Normal;
if let Some(family) = ui_family.clone() {
style.font_family = family;
}
return SelectableText::view(
SelectableTextOptions {
id: ElementId::from(self.id.clone()),
text: self.content.clone(),
runs: vec![style.to_run(self.content.len())],
font_size,
line_height,
text_color,
wrap: SelectableTextWrap::Normal,
key_context: "SelectableText",
fill_width: true,
font_family: ui_family.clone(),
},
window,
cx,
);
}
let mut title = gpui::div()
.text_size(font_size)
.line_height(line_height)
.font_weight(weight)
.text_color(text_color)
.child(self.content.clone());
if let Some(family) = ui_family {
title = title.font_family(family);
}
title.into_any_element()
}
}
impl RenderOnce for Title {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.global::<Config>().theme.clone();
self.render_with_theme(&theme, _window, cx)
}
}
impl IntoElement for Title {
type Element = Component<Self>;
fn into_element(self) -> Self::Element {
Component::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn title_default_id_is_stable_across_render_rebuilds() {
let source = include_str!("title.rs")
.split("#[cfg(test)]")
.next()
.unwrap();
assert!(source.contains("#[track_caller]"));
assert!(source.contains("fn default_title_id"));
assert!(!source.contains(r#"liora_core::unique_id("title")"#));
}
#[test]
fn title_defaults_to_mouse_selectable() {
assert!(Title::new("Selectable title").selectable);
}
#[test]
fn title_uses_selectable_text_for_native_selection() {
let source = include_str!("title.rs");
assert!(source.contains("SelectableText::view"));
assert!(source.contains("pub fn selectable"));
assert!(source.contains("pub fn register_key_bindings"));
}
}