cursive_core/views/
panel.rs

1use crate::align::*;
2use crate::event::{Event, EventResult};
3use crate::rect::Rect;
4use crate::style::PaletteStyle;
5use crate::utils::markup::StyledString;
6use crate::view::{View, ViewWrapper};
7use crate::Printer;
8use crate::Vec2;
9use crate::With;
10
11/// Draws a border around a wrapped view.
12#[derive(Debug)]
13pub struct Panel<V> {
14    // Inner view
15    view: V,
16
17    // Possibly empty title.
18    title: StyledString,
19
20    // Where to put the title position
21    title_position: HAlign,
22
23    // `true` when we needs to relayout
24    invalidated: bool,
25}
26
27new_default!(Panel<V: Default>);
28
29/// Minimum distance between title and borders.
30const TITLE_SPACING: usize = 3;
31
32impl<V> Panel<V> {
33    /// Creates a new panel around the given view.
34    pub fn new(view: V) -> Self {
35        Panel {
36            view,
37            title: StyledString::new(),
38            title_position: HAlign::Center,
39            invalidated: true,
40        }
41    }
42
43    /// Sets the title of the dialog.
44    ///
45    /// If not empty, it will be visible at the top.
46    #[must_use]
47    pub fn title<S: Into<StyledString>>(self, label: S) -> Self {
48        self.with(|s| s.set_title(label))
49    }
50
51    /// Sets the title of the dialog.
52    pub fn set_title<S: Into<StyledString>>(&mut self, label: S) {
53        self.title = label.into();
54        self.invalidate();
55    }
56
57    /// Sets the horizontal position of the title in the dialog.
58    /// The default position is `HAlign::Center`
59    #[must_use]
60    pub fn title_position(self, align: HAlign) -> Self {
61        self.with(|s| s.set_title_position(align))
62    }
63
64    /// Sets the horizontal position of the title in the dialog.
65    /// The default position is `HAlign::Center`
66    pub fn set_title_position(&mut self, align: HAlign) {
67        self.title_position = align;
68    }
69
70    fn draw_title(&self, printer: &Printer) {
71        if !self.title.is_empty() {
72            let available = match printer.size.x.checked_sub(2 * TITLE_SPACING) {
73                Some(available) => available,
74                None => return, /* Panel is too small to even write the decoration. */
75            };
76            let len = std::cmp::min(self.title.width(), available);
77            let x = TITLE_SPACING + self.title_position.get_offset(len, available);
78
79            printer
80                .offset((x, 0))
81                .cropped((len, 1))
82                .with_style(PaletteStyle::TitlePrimary, |p| {
83                    p.print_styled((0, 0), &self.title)
84                });
85            printer.with_high_border(false, |printer| {
86                printer.print((x - 2, 0), "┤ ");
87                printer.print((x + len, 0), " ├");
88            });
89        }
90    }
91
92    fn invalidate(&mut self) {
93        self.invalidated = true;
94    }
95
96    inner_getters!(self.view: V);
97}
98
99impl<V: View> ViewWrapper for Panel<V> {
100    wrap_impl!(self.view: V);
101
102    fn wrap_on_event(&mut self, event: Event) -> EventResult {
103        self.view.on_event(event.relativized((1, 1)))
104    }
105
106    fn wrap_required_size(&mut self, req: Vec2) -> Vec2 {
107        // TODO: make borders conditional?
108        let req = req.saturating_sub((2, 2));
109
110        let size = self.view.required_size(req) + (2, 2);
111        if self.title.is_empty() {
112            size
113        } else {
114            let title_width = self.title.width() + 2 * TITLE_SPACING;
115            size.or_max((title_width, 0))
116        }
117    }
118
119    fn wrap_draw(&self, printer: &Printer) {
120        printer.print_box((0, 0), printer.size, true);
121        self.draw_title(printer);
122
123        let printer = printer.offset((1, 1)).shrinked((1, 1));
124        self.view.draw(&printer);
125    }
126
127    fn wrap_layout(&mut self, size: Vec2) {
128        self.view.layout(size.saturating_sub((2, 2)));
129    }
130
131    fn wrap_important_area(&self, size: Vec2) -> Rect {
132        let inner_size = size.saturating_sub((2, 2));
133        self.view.important_area(inner_size) + (1, 1)
134    }
135
136    fn wrap_needs_relayout(&self) -> bool {
137        self.invalidated || self.view.needs_relayout()
138    }
139}
140
141#[crate::blueprint(Panel::new(view))]
142struct Blueprint {
143    view: crate::views::BoxedView,
144
145    title: Option<StyledString>,
146    title_position: Option<HAlign>,
147}
148
149// TODO: reduce code duplication between blueprints for the same view.
150crate::manual_blueprint!(with panel, |config, context| {
151    let title = match config {
152        crate::builder::Config::String(_) => context.resolve(config)?,
153        crate::builder::Config::Object(config) => {
154            match config.get("title") {
155                Some(title) => context.resolve(title)?,
156                None => StyledString::new()
157            }
158        }
159        _ => StyledString::new(),
160    };
161
162    let title_position = context.resolve(&config["title_position"])?;
163
164    Ok(move |view| {
165        let mut panel = crate::views::Panel::new(view).title(title);
166
167        if let Some(title_position) = title_position {
168            panel.set_title_position(title_position);
169        }
170
171        panel
172    })
173});