Skip to main content

gpui/
util.rs

1use crate::{BackgroundExecutor, Task};
2use std::{
3    future::Future,
4    pin::Pin,
5    sync::atomic::{AtomicUsize, Ordering::SeqCst},
6    task,
7    time::Duration,
8};
9
10/// Adds conditional methods that keep a builder chain in one readable sequence.
11///
12/// Implemented for every [`IntoElement`](crate::IntoElement) type and for window option types.
13/// The trait is re-exported by [`gpui::prelude`](crate::prelude).
14///
15/// # Examples
16///
17/// ```
18/// use gpui::{div, prelude::*};
19///
20/// let selected = true;
21/// let row = div().when(selected, |row| row.flex());
22/// ```
23pub trait FluentBuilder {
24    /// Passes `self` to `f`, and returns the result. Use it to continue a chain with a
25    /// closure.
26    fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
27    where
28        Self: Sized,
29    {
30        f(self)
31    }
32
33    /// Calls `then` with `self` if `condition` is true. Otherwise returns `self`
34    /// unchanged.
35    fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
36    where
37        Self: Sized,
38    {
39        self.map(|this| if condition { then(this) } else { this })
40    }
41
42    /// Calls `then` with `self` if `condition` is true, and `else_fn` with `self`
43    /// otherwise.
44    fn when_else(
45        self,
46        condition: bool,
47        then: impl FnOnce(Self) -> Self,
48        else_fn: impl FnOnce(Self) -> Self,
49    ) -> Self
50    where
51        Self: Sized,
52    {
53        self.map(|this| if condition { then(this) } else { else_fn(this) })
54    }
55
56    /// Calls `then` with `self` and the value inside `option` if the option is `Some`.
57    /// Returns `self` unchanged for `None`.
58    fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
59    where
60        Self: Sized,
61    {
62        self.map(|this| {
63            if let Some(value) = option {
64                then(this, value)
65            } else {
66                this
67            }
68        })
69    }
70    /// Calls `then` with `self` if `option` is `None`. The option is taken by reference,
71    /// so you can still use it afterward.
72    fn when_none<T>(self, option: &Option<T>, then: impl FnOnce(Self) -> Self) -> Self
73    where
74        Self: Sized,
75    {
76        self.map(|this| if option.is_some() { this } else { then(this) })
77    }
78}
79
80/// Adds timeout combinators to futures.
81pub trait FutureExt {
82    /// Limits how long the future may wait for a result.
83    ///
84    /// The result is `Err(`[`Timeout`]`)` if `timeout` elapses before the future completes. The
85    /// timer comes from `executor`, so tests can advance it with the simulated clock.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use std::time::Duration;
91    /// use gpui::{FutureExt, TestApp};
92    ///
93    /// let app = TestApp::new();
94    /// let slow = app.background_executor().timer(Duration::from_secs(60));
95    /// let result = app
96    ///     .foreground_executor()
97    ///     .block_test(slow.with_timeout(Duration::from_secs(1), app.background_executor()));
98    /// assert!(result.is_err());
99    /// ```
100    fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
101    where
102        Self: Sized;
103}
104
105impl<T: Future> FutureExt for T {
106    fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
107    where
108        Self: Sized,
109    {
110        WithTimeout {
111            future: self,
112            timer: executor.timer(timeout),
113        }
114    }
115}
116
117/// UTF-8 text range containment checks.
118pub trait TextRangeExt {
119    /// Returns whether `range` is ordered, in bounds, and lies on character boundaries.
120    fn contains_range(&self, range: &std::ops::Range<usize>) -> bool;
121}
122
123impl TextRangeExt for str {
124    fn contains_range(&self, range: &std::ops::Range<usize>) -> bool {
125        self.get(range.clone()).is_some()
126    }
127}
128
129/// The future returned by [`FutureExt::with_timeout`]. It resolves to the wrapped
130/// future's output, or to [`Timeout`] if the timer fires first.
131#[pin_project::pin_project]
132pub struct WithTimeout<T> {
133    #[pin]
134    future: T,
135    #[pin]
136    timer: Task<()>,
137}
138
139/// The error returned when a future given to [`FutureExt::with_timeout`] doesn't
140/// finish in time.
141#[derive(Debug, thiserror::Error)]
142#[error("Timed out before future resolved")]
143pub struct Timeout;
144
145impl<T: Future> Future for WithTimeout<T> {
146    type Output = Result<T::Output, Timeout>;
147
148    fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll<Self::Output> {
149        let this = self.project();
150
151        if let task::Poll::Ready(output) = this.future.poll(cx) {
152            task::Poll::Ready(Ok(output))
153        } else if this.timer.poll(cx).is_ready() {
154            task::Poll::Ready(Err(Timeout))
155        } else {
156            task::Poll::Pending
157        }
158    }
159}
160
161/// Increment the given atomic counter if it is not zero.
162/// Return the new value of the counter.
163pub(crate) fn atomic_incr_if_not_zero(counter: &AtomicUsize) -> usize {
164    let mut loaded = counter.load(SeqCst);
165    loop {
166        if loaded == 0 {
167            return 0;
168        }
169        match counter.compare_exchange_weak(loaded, loaded + 1, SeqCst, SeqCst) {
170            Ok(x) => return x + 1,
171            Err(actual) => loaded = actual,
172        }
173    }
174}
175
176/// Rounds to the nearest integer with 0.5 ties toward zero.
177#[inline]
178pub(crate) fn round_half_toward_zero(value: f32) -> f32 {
179    (value.abs() - 0.5).ceil().copysign(value)
180}
181
182#[inline]
183pub(crate) fn round_half_toward_zero_f64(value: f64) -> f64 {
184    (value.abs() - 0.5).ceil().copysign(value)
185}
186
187#[inline]
188pub(crate) fn round_to_device_pixel(logical: f32, scale_factor: f32) -> f32 {
189    round_half_toward_zero(logical * scale_factor)
190}
191
192#[inline]
193pub(crate) fn round_stroke_to_device_pixel(logical: f32, scale_factor: f32) -> f32 {
194    if logical == 0.0 {
195        0.0
196    } else {
197        round_to_device_pixel(logical.max(0.0), scale_factor).max(1.0)
198    }
199}
200
201#[inline]
202pub(crate) fn floor_to_device_pixel(logical: f32, scale_factor: f32) -> f32 {
203    (logical * scale_factor).floor()
204}
205
206#[inline]
207pub(crate) fn ceil_to_device_pixel(logical: f32, scale_factor: f32) -> f32 {
208    (logical * scale_factor).ceil()
209}
210
211#[cfg(test)]
212mod tests {
213    use crate::TestAppContext;
214
215    use super::*;
216
217    #[test]
218    fn test_round_half_toward_zero() {
219        // Midpoint ties go toward zero
220        assert_eq!(round_half_toward_zero(0.5), 0.0);
221        assert_eq!(round_half_toward_zero(1.5), 1.0);
222        assert_eq!(round_half_toward_zero(2.5), 2.0);
223        assert_eq!(round_half_toward_zero(-0.5), 0.0);
224        assert_eq!(round_half_toward_zero(-1.5), -1.0);
225        assert_eq!(round_half_toward_zero(-2.5), -2.0);
226
227        // Non-midpoint values round to nearest
228        assert_eq!(round_half_toward_zero(1.5001), 2.0);
229        assert_eq!(round_half_toward_zero(1.4999), 1.0);
230        assert_eq!(round_half_toward_zero(-1.5001), -2.0);
231        assert_eq!(round_half_toward_zero(-1.4999), -1.0);
232
233        // Integers are unchanged
234        assert_eq!(round_half_toward_zero(0.0), 0.0);
235        assert_eq!(round_half_toward_zero(3.0), 3.0);
236        assert_eq!(round_half_toward_zero(-3.0), -3.0);
237    }
238
239    #[test]
240    fn test_device_pixel_helpers() {
241        // Snap uses half-toward-zero: 1.0 * 1.5 = 1.5 ties toward 1.0.
242        assert_eq!(round_to_device_pixel(1.0, 1.5), 1.0);
243        // Below the tie rounds down, above rounds up.
244        assert_eq!(round_to_device_pixel(0.3, 2.0), 1.0);
245        assert_eq!(round_to_device_pixel(1.4, 1.0), 1.0);
246        assert_eq!(round_to_device_pixel(1.6, 1.0), 2.0);
247
248        // Stroke uses snap, but clamps non-zero input up to at least 1dp.
249        assert_eq!(round_stroke_to_device_pixel(0.0, 1.0), 0.0);
250        assert_eq!(round_stroke_to_device_pixel(0.4, 1.0), 1.0);
251        assert_eq!(round_stroke_to_device_pixel(0.5, 1.0), 1.0);
252        assert_eq!(round_stroke_to_device_pixel(1.0, 1.5), 1.0);
253        assert_eq!(round_stroke_to_device_pixel(1.6, 1.0), 2.0);
254
255        // Cover's near edge floors, far edge ceils. Together they form a strict superset.
256        assert_eq!(floor_to_device_pixel(0.3, 2.0), 0.0);
257        assert_eq!(ceil_to_device_pixel(0.3, 2.0), 1.0);
258        assert_eq!(floor_to_device_pixel(2.1, 1.0), 2.0);
259        assert_eq!(ceil_to_device_pixel(2.1, 1.0), 3.0);
260
261        // Integer device-pixel inputs are stable under all three.
262        assert_eq!(round_to_device_pixel(2.0, 2.0), 4.0);
263        assert_eq!(floor_to_device_pixel(2.0, 2.0), 4.0);
264        assert_eq!(ceil_to_device_pixel(2.0, 2.0), 4.0);
265    }
266
267    #[test]
268    fn test_round_half_toward_zero_f64() {
269        assert_eq!(round_half_toward_zero_f64(0.5), 0.0);
270        assert_eq!(round_half_toward_zero_f64(-0.5), 0.0);
271        assert_eq!(round_half_toward_zero_f64(1.5), 1.0);
272        assert_eq!(round_half_toward_zero_f64(-1.5), -1.0);
273        assert_eq!(round_half_toward_zero_f64(2.5001), 3.0);
274    }
275
276    #[gpui::test]
277    async fn test_with_timeout(cx: &mut TestAppContext) {
278        Task::ready(())
279            .with_timeout(Duration::from_secs(1), &cx.executor())
280            .await
281            .expect("Timeout should be noop");
282
283        let long_duration = Duration::from_secs(6000);
284        let short_duration = Duration::from_secs(1);
285        cx.executor()
286            .timer(long_duration)
287            .with_timeout(short_duration, &cx.executor())
288            .await
289            .expect_err("timeout should have triggered");
290
291        let fut = cx
292            .executor()
293            .timer(long_duration)
294            .with_timeout(short_duration, &cx.executor());
295        cx.executor().advance_clock(short_duration * 2);
296        futures::FutureExt::now_or_never(fut)
297            .unwrap_or_else(|| panic!("timeout should have triggered"))
298            .expect_err("timeout");
299    }
300}