godot_core/meta/
signed_range.rs

1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8use std::collections::Bound;
9use std::ops::RangeBounds;
10
11/// Range which can be wrapped.
12///
13/// Constructed with [`wrapped()`] utility function.
14struct WrappedRange {
15    lower_bound: i64,
16    upper_bound: Option<i64>,
17}
18
19/// Accepts negative bounds, interpreted relative to the end of the collection.
20///
21/// ## Examples
22/// ```no_run
23/// # use godot::meta::wrapped;
24/// wrapped(1..-2); // from 1 to len-2
25/// wrapped(..-2);  // from 0 to len-2
26/// wrapped(-3..);  // from len-3 to end
27/// wrapped(-4..3); // from len-4 to 3
28/// ```
29pub fn wrapped<T>(signed_range: impl RangeBounds<T>) -> impl SignedRange
30where
31    T: Copy + Into<i64>,
32{
33    let lower_bound = lower_bound(signed_range.start_bound().map(|v| (*v).into())).unwrap_or(0);
34    let upper_bound = upper_bound(signed_range.end_bound().map(|v| (*v).into()));
35
36    WrappedRange {
37        lower_bound,
38        upper_bound,
39    }
40}
41
42fn lower_bound(bound: Bound<i64>) -> Option<i64> {
43    match bound {
44        Bound::Included(n) => Some(n),
45        Bound::Excluded(n) => Some(n + 1),
46        Bound::Unbounded => None,
47    }
48}
49
50fn upper_bound(bound: Bound<i64>) -> Option<i64> {
51    match bound {
52        Bound::Included(n) => Some(n + 1),
53        Bound::Excluded(n) => Some(n),
54        Bound::Unbounded => None,
55    }
56}
57
58mod sealed {
59    pub trait SealedRange {}
60}
61
62/// Trait supporting regular `usize` ranges, as well as negative indices.
63///
64/// If a lower or upper bound is negative, then its value is relative to the end of the given collection.  \
65/// Use the [`wrapped()`] utility function to construct such ranges.
66pub trait SignedRange: sealed::SealedRange {
67    /// Returns a tuple of `(from, to)` from a Rust range.
68    /// Unbounded upper range is represented by `None`.
69    // Note: in some cases unbounded upper bounds should be represented by `i32::MAX` instead of `i64::MAX`,
70    // since Godot treats some indexes as 32-bit despite being declared as `i64` in GDExtension API.
71    #[doc(hidden)]
72    fn signed(&self) -> (i64, Option<i64>);
73}
74
75impl sealed::SealedRange for WrappedRange {}
76impl SignedRange for WrappedRange {
77    fn signed(&self) -> (i64, Option<i64>) {
78        (self.lower_bound, self.upper_bound)
79    }
80}
81
82impl<R> sealed::SealedRange for R where R: RangeBounds<usize> {}
83impl<R> SignedRange for R
84where
85    R: RangeBounds<usize>,
86{
87    fn signed(&self) -> (i64, Option<i64>) {
88        let lower_bound = lower_bound(self.start_bound().map(|v| *v as i64)).unwrap_or(0);
89        let upper_bound = upper_bound(self.end_bound().map(|v| *v as i64));
90
91        (lower_bound, upper_bound)
92    }
93}
94
95/// Returns a tuple of `(from, to)` from a Rust range.
96///
97/// # Panics (safeguards-strict)
98/// When `from` > `to`.
99pub(crate) fn to_godot_range_fromto(range: impl SignedRange) -> (i64, i64) {
100    match range.signed() {
101        (from, Some(to)) => {
102            crate::sys::strict_assert!(from <= to, "range: start ({from}) > end ({to})");
103            (from, to)
104        }
105        (from, None) => (from, 0),
106    }
107}
108
109/// Returns a tuple of `(from, len)` from a Rust range.
110///
111/// # Panics
112/// In debug mode, when from > to.
113pub(crate) fn to_godot_range_fromlen(range: impl SignedRange, unbounded: i64) -> (i64, i64) {
114    match range.signed() {
115        (from, Some(to)) => {
116            crate::sys::strict_assert!(from <= to, "range: start ({from}) > end ({to})");
117            (from, to - from)
118        }
119        (from, None) => (from, unbounded),
120    }
121}