Unnamed repository; edit this file 'description' to name the repository.
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
use std::borrow::Cow;

use helix_view::{graphics::Rect, Editor};
use tui::{
    buffer::Buffer as Surface,
    widgets::{Block, Widget as _},
};

use crate::compositor::{Component, Context, Event, EventResult};

use super::{menu::Item, Menu, PromptEvent, Text};

pub struct Select<T: Item> {
    message: Text,
    options: Menu<T>,
}

impl<T: Item> Select<T> {
    pub fn new<M, I, F>(message: M, options: I, data: T::Data, callback: F) -> Self
    where
        M: Into<Cow<'static, str>>,
        I: IntoIterator<Item = T>,
        F: Fn(&mut Editor, &T, PromptEvent) + 'static,
    {
        let message = tui::text::Text::from(message.into()).into();
        let options: Vec<_> = options.into_iter().collect();
        assert!(!options.is_empty());
        let mut menu = Menu::new(options, data, move |editor, option, event| {
            // Options are non-empty (asserted above) and an option is selected by default,
            // so `option` must be Some here.
            let option = &option.unwrap();
            callback(editor, option, event)
        })
        .auto_close(true);
        // Select the first option by default.
        menu.move_down();

        Self {
            message,
            options: menu,
        }
    }
}

impl<T: Item> Component for Select<T> {
    fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
        self.options.handle_event(event, cx)
    }

    fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
        let (message_width, message_height) = self.message.required_size(viewport).unwrap();
        let (menu_width, menu_height) = self.options.required_size(viewport).unwrap();
        Some((
            menu_width.max(message_width + 2),
            message_height + menu_height + 2,
        ))
    }

    fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
        const BLOCK: Block<'_> = Block::bordered();

        // +---------------------+
        // | message             |
        // +---------------------+
        //   options menu
        //
        //

        // Limit the text width to 80% of the screen or 80 columns, whichever is
        // smaller.
        let max_width = 80.min(((area.width as u32) * 80u32 / 100) as u16);
        let (message_width, message_height) =
            super::text::required_size(&self.message.contents, max_width);
        let (_, menu_height) = self
            .options
            .required_size((max_width, area.height))
            .unwrap();
        // + 2 for borders and another + 2 for horizontal padding
        let width = message_width + 4;
        let height = message_height + 2 + menu_height;
        let area = Rect {
            x: (area.width / 2) - width / 2,
            y: (area.height / 2) - height / 2,
            width,
            height,
        };

        // Message
        let background = cx.editor.theme.get("ui.background");
        let text = cx.editor.theme.get("ui.text");
        let message_box = area.with_height(message_height + 2);
        surface.clear_with(message_box, background.patch(text));
        BLOCK.render(message_box, surface);
        // Add horizontal padding so the message isn't too close to the border.
        let message_area = BLOCK.inner(message_box).clip_left(1).clip_right(1);
        self.message.render(message_area, surface, cx);

        // Options menu
        let menu_area = area.clip_top(message_height + 2);
        self.options.render(menu_area, surface, cx);
    }
}