Skip to main content

egui_components/
heading.rs

1//! `Heading` — section title typography.
2//!
3//! A heading renders a strong title at one of four levels (`H1`–`H4`) with an
4//! optional muted description line underneath, matching the shadcn/typography
5//! section-title pattern. Upstream gpui-component has no dedicated heading type
6//! (its `Text` primitive is a full rich-text view); this is the lightweight
7//! egui-idiomatic equivalent used to label pages and sections.
8//!
9//! ```ignore
10//! ui.add(sc::Heading::new("Settings"));                       // H2 (default)
11//! ui.add(sc::Heading::new("Welcome back").level(sc::HeadingLevel::H1));
12//! ui.add(
13//!     sc::Heading::new("Notifications")
14//!         .level(sc::HeadingLevel::H3)
15//!         .description("Choose what you want to be notified about."),
16//! );
17//! ```
18
19use egui::{FontId, Response, RichText, Ui, Widget};
20use egui_components_theme::Theme;
21
22/// Heading level — controls the title's font size (and, transitively, its
23/// visual weight in the hierarchy). Smaller number = larger / more prominent.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
25pub enum HeadingLevel {
26    H1,
27    #[default]
28    H2,
29    H3,
30    H4,
31}
32
33impl HeadingLevel {
34    /// Font size for the title at this level.
35    fn font_size(&self) -> f32 {
36        match self {
37            HeadingLevel::H1 => 30.0,
38            HeadingLevel::H2 => 24.0,
39            HeadingLevel::H3 => 19.0,
40            HeadingLevel::H4 => 16.0,
41        }
42    }
43}
44
45/// A section title with an optional description sub-line.
46pub struct Heading {
47    text: String,
48    level: HeadingLevel,
49    description: Option<String>,
50}
51
52impl Heading {
53    pub fn new(text: impl Into<String>) -> Self {
54        Self {
55            text: text.into(),
56            level: HeadingLevel::default(),
57            description: None,
58        }
59    }
60
61    pub fn level(mut self, level: HeadingLevel) -> Self {
62        self.level = level;
63        self
64    }
65
66    pub fn h1(self) -> Self {
67        self.level(HeadingLevel::H1)
68    }
69    pub fn h2(self) -> Self {
70        self.level(HeadingLevel::H2)
71    }
72    pub fn h3(self) -> Self {
73        self.level(HeadingLevel::H3)
74    }
75    pub fn h4(self) -> Self {
76        self.level(HeadingLevel::H4)
77    }
78
79    /// A muted description rendered on its own line beneath the title.
80    pub fn description(mut self, text: impl Into<String>) -> Self {
81        self.description = Some(text.into());
82        self
83    }
84}
85
86impl Widget for Heading {
87    fn ui(self, ui: &mut Ui) -> Response {
88        let theme = Theme::get(ui.ctx());
89        let c = theme.colors;
90        ui.vertical(|ui| {
91            ui.add(egui::Label::new(
92                RichText::new(self.text)
93                    .color(c.foreground)
94                    .font(FontId::proportional(self.level.font_size()))
95                    .strong(),
96            ));
97            if let Some(desc) = self.description {
98                ui.add_space(2.0);
99                ui.add(egui::Label::new(
100                    RichText::new(desc)
101                        .color(c.muted_foreground)
102                        .font(FontId::proportional(theme.metrics.font_size_sm)),
103                ));
104            }
105        })
106        .response
107    }
108}