aboutsummaryrefslogtreecommitdiffstats
path: root/sl0pstatus/src/widgets/new_tab.rs
blob: deaea84e0a4360edb7875837f0a023437597458c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
use std::collections::BTreeMap;

use zellij_tile::shim::new_tab;

use crate::{config::ZellijState, render::FormattedPart, widgets::widget::Widget};

/// sl0p fork: a clickable "new tab" button for the bar.
///
/// The tab strip is already a mouse surface -- clicking a tab switches to it,
/// double-clicking renames it -- but the one thing you cannot do with the mouse
/// is make a new one, which is the operation a tab strip in every other
/// application offers. `{new_tab}` fills that hole.
///
/// Configured like every other widget:
///
/// ```kdl
/// new_tab "#[bg=$bg,fg=$dim] + "
/// ```
///
/// Rendering and hit-testing both come from this one string: the button covers
/// exactly the columns it draws, because `process_widget_click` measures the
/// same rendered output it placed. There is nothing here that can drift out of
/// alignment with what you see -- unlike a pane frame's button, whose geometry
/// is computed twice.
pub struct NewTabWidget {
    format: FormattedPart,
}

impl NewTabWidget {
    pub fn new(config: &BTreeMap<String, String>) -> Self {
        let format = match config.get("new_tab") {
            Some(format) => FormattedPart::from_format_string(format, config),
            // A default that is visible without any configuration: the widget
            // is only rendered if the user puts {new_tab} in a format string,
            // so an empty button would just be a confusing dead zone.
            None => FormattedPart::from_format_string(" + ", config),
        };

        Self { format }
    }
}

impl Widget for NewTabWidget {
    fn process(&self, _name: &str, _state: &ZellijState) -> String {
        self.format.format_string(&self.format.content)
    }

    fn process_click(&self, _name: &str, _state: &ZellijState, _pos: usize, _is_double_click: bool) {
        // No name and no cwd: the same tab the `n` keybinding makes, so the
        // button is the mouse spelling of a thing that already exists rather
        // than a second, subtly different way to make a tab. The new tab takes
        // focus, which is what makes this a button and not a counter.
        new_tab(None::<String>, None::<String>);
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn renders_the_configured_button() {
        let mut config = BTreeMap::new();
        config.insert("new_tab".to_owned(), "#[fg=#ffffff] + ".to_owned());
        let widget = NewTabWidget::new(&config);

        let output = widget.process("new_tab", &ZellijState::default());

        assert!(
            output.contains(" + "),
            "expected the button text in {output:?}"
        );
    }

    #[test]
    fn renders_something_clickable_without_configuration() {
        // an unconfigured {new_tab} must not be a zero-width click target
        let widget = NewTabWidget::new(&BTreeMap::new());

        let output = widget.process("new_tab", &ZellijState::default());

        assert!(
            console::measure_text_width(&output) > 0,
            "the default button has no width: {output:?}"
        );
    }
}