Skip to main content

rusty_bubbles/
cursor.rs

1//! Cleanroom Rust port of upstream Go source file: `cursor/cursor.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # Cursor
6//!
7//! A virtual cursor to support the textinput and textarea elements.
8//! </public-docs>
9
10use rusty_bubbletea::model::{Cmd, Msg};
11use rusty_lipgloss::Style;
12use std::fmt;
13use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
14use std::sync::Arc;
15use std::time::Duration;
16
17const DEFAULT_BLINK_SPEED: Duration = Duration::from_millis(530);
18
19/// Internal ID management. Used during animating to ensure that frame
20/// messages are received only by spinner components that sent them.
21static LAST_ID: AtomicI64 = AtomicI64::new(0);
22
23fn next_id() -> i32 {
24    (LAST_ID.fetch_add(1, Ordering::SeqCst)) as i32
25}
26
27/// initialBlinkMsg initializes cursor blinking.
28#[derive(Debug)]
29struct InitialBlinkMsg;
30
31/// BlinkMsg signals that the cursor should blink. It contains metadata that
32/// allows us to tell if the blink message is the one we're expecting.
33#[derive(Debug, Clone)]
34pub struct BlinkMsg {
35    id: i32,
36    tag: i32,
37}
38
39/// blinkCanceled is sent when a blink operation is canceled.
40#[derive(Debug)]
41struct BlinkCanceled;
42
43/// Mode describes the behavior of the cursor.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Mode {
46    /// The cursor blinks.
47    Blink,
48    /// The cursor is static.
49    Static,
50    /// The cursor is hidden.
51    Hide,
52}
53
54impl Mode {
55    /// Returns the cursor mode in a human-readable format. This method is
56    /// provisional and for informational purposes only.
57    pub fn to_string(&self) -> &'static str {
58        match self {
59            Mode::Blink => "blink",
60            Mode::Static => "static",
61            Mode::Hide => "hidden",
62        }
63    }
64}
65
66/// Model is the Bubble Tea model for this cursor element.
67#[derive(Clone)]
68pub struct Model {
69    /// Style styles the cursor block.
70    pub style: Style,
71
72    /// TextStyle is the style used for the cursor when it is blinking
73    /// (hidden), i.e. displaying normal text.
74    pub text_style: Style,
75
76    /// BlinkSpeed is the speed at which the cursor blinks. This has no effect
77    /// unless [`Mode::Blink`] is set.
78    pub blink_speed: Duration,
79
80    /// IsBlinked is the state of the cursor blink. When true, the cursor is
81    /// hidden.
82    pub is_blinked: bool,
83
84    /// char is the character under the cursor
85    char: String,
86
87    /// The ID of this Model as it relates to other cursors
88    id: i32,
89
90    /// focus indicates whether the containing input is focused
91    focus: bool,
92
93    /// Used to manage cursor blink cancellation.
94    blink_cancel: Option<Arc<AtomicBool>>,
95
96    /// The ID of the blink message we're expecting to receive.
97    blink_tag: i32,
98
99    /// mode determines the behavior of the cursor
100    mode: Mode,
101}
102
103impl fmt::Debug for Model {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.debug_struct("cursor::Model")
106            .field("mode", &self.mode)
107            .field("focus", &self.focus)
108            .field("is_blinked", &self.is_blinked)
109            .finish()
110    }
111}
112
113/// New creates a new model with default settings.
114pub fn new() -> Model {
115    Model {
116        id: next_id(),
117        blink_speed: DEFAULT_BLINK_SPEED,
118        is_blinked: true,
119        mode: Mode::Blink,
120        style: Style::new(),
121        text_style: Style::new(),
122        char: String::new(),
123        focus: false,
124        blink_cancel: None,
125        blink_tag: 0,
126    }
127}
128
129impl Model {
130    /// Update updates the cursor.
131    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
132        if msg.as_any().downcast_ref::<InitialBlinkMsg>().is_some() {
133            if self.mode != Mode::Blink || !self.focus {
134                return None;
135            }
136            return self.blink();
137        }
138
139        if msg
140            .as_any()
141            .downcast_ref::<rusty_bubbletea::focus::FocusMsg>()
142            .is_some()
143        {
144            return self.focus();
145        }
146
147        if msg
148            .as_any()
149            .downcast_ref::<rusty_bubbletea::focus::BlurMsg>()
150            .is_some()
151        {
152            self.blur();
153            return None;
154        }
155
156        if let Some(m) = msg.as_any().downcast_ref::<BlinkMsg>() {
157            // We're choosy about whether to accept blinkMsgs so that our
158            // cursor only blinks exactly when it should.
159
160            // Is this model blink-able?
161            if self.mode != Mode::Blink || !self.focus {
162                return None;
163            }
164
165            // Were we expecting this blink message?
166            if m.id != self.id || m.tag != self.blink_tag {
167                return None;
168            }
169
170            let mut cmd = None;
171            if self.mode == Mode::Blink {
172                self.is_blinked = !self.is_blinked;
173                cmd = self.blink();
174            }
175            return cmd;
176        }
177
178        if msg.as_any().downcast_ref::<BlinkCanceled>().is_some() {
179            // no-op
180            return None;
181        }
182
183        None
184    }
185
186    /// Mode returns the model's cursor mode. For available cursor modes, see
187    /// [`Mode`].
188    pub fn mode(&self) -> Mode {
189        self.mode
190    }
191
192    /// SetMode sets the model's cursor mode. This method returns a command.
193    ///
194    /// For available cursor modes, see [`Mode`].
195    pub fn set_mode(&mut self, mode: Mode) -> Cmd {
196        self.mode = mode;
197        self.is_blinked = mode == Mode::Hide || !self.focus;
198        if mode == Mode::Blink {
199            return Some(Box::new(|| Some(Box::new(InitialBlinkMsg))));
200        }
201        None
202    }
203
204    /// Blink is a command used to manage cursor blinking.
205    pub fn blink(&mut self) -> Cmd {
206        if self.mode != Mode::Blink {
207            return None;
208        }
209
210        if let Some(cancel) = &self.blink_cancel {
211            cancel.store(true, Ordering::SeqCst);
212        }
213
214        let cancel = Arc::new(AtomicBool::new(false));
215        self.blink_cancel = Some(cancel.clone());
216
217        self.blink_tag += 1;
218        let blink_msg = BlinkMsg {
219            id: self.id,
220            tag: self.blink_tag,
221        };
222        let speed = self.blink_speed;
223
224        Some(Box::new(move || {
225            std::thread::sleep(speed);
226            if cancel.load(Ordering::SeqCst) {
227                Some(Box::new(BlinkCanceled))
228            } else {
229                Some(Box::new(blink_msg))
230            }
231        }))
232    }
233
234    /// Focus focuses the cursor to allow it to blink if desired.
235    pub fn focus(&mut self) -> Cmd {
236        self.focus = true;
237        // show the cursor unless we've explicitly hidden it
238        self.is_blinked = self.mode == Mode::Hide;
239
240        if self.mode == Mode::Blink && self.focus {
241            return self.blink();
242        }
243        None
244    }
245
246    /// Blur blurs the cursor.
247    pub fn blur(&mut self) {
248        self.focus = false;
249        self.is_blinked = true;
250    }
251
252    /// SetChar sets the character under the cursor.
253    pub fn set_char(&mut self, char: &str) {
254        self.char = char.to_string();
255    }
256
257    /// View displays the cursor.
258    pub fn view(&self) -> String {
259        if self.is_blinked {
260            self.text_style.clone().inline(true).render(&self.char)
261        } else {
262            self.style
263                .clone()
264                .inline(true)
265                .reverse(true)
266                .render(&self.char)
267        }
268    }
269}
270
271/// Blink is a command used to initialize cursor blinking.
272pub fn blink() -> Box<dyn Msg> {
273    Box::new(InitialBlinkMsg)
274}