nandi/jolt-nativepublic Fork 0
121e5f1d751e8003ea229e70debf486b9bea3286
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Keep asking until the row is there, and say so when it is not b49f82a · on 121e5f1d751e8003ea229e70debf486b9bea3286 · nandi · 7d ago
rows.rs · 253 lines · 7.8 KBRust Blame HistoryRaw
  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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
//! A column that remembers where it put its rows.
//!
//! iced can scroll a list to an offset and to nothing else: `snap_to`,
//! `scroll_to` and `scroll_by` all take a number of points, and there is no
//! "show me this child" anywhere in the toolkit. So a client that wants to be
//! taken to one row of a hundred has to answer the question itself — how far
//! down the content is that row? — and the only thing that knows is the layout.
//!
//! This is that, in the one place the answer is free. `Rows` wraps the column
//! inside a scroll area and does nothing to it but read the layout it already
//! computed, writing down where each row landed. A jump is then a lookup and
//! the `scroll_to` that already exists.
//!
//! It is one widget per scroll area, always, whether or not anything is asking
//! to be scrolled to. That is the whole of its safety: iced matches widgets by
//! where they sit, so a wrapper that came and went as rows became interesting
//! would leave the tree a different shape than the state kept for it. The
//! `State` below is why it can be told apart from the column it wraps at all —
//! iced tells two widgets apart by the type of their state, and two stateless
//! ones are the same widget as far as it knows.

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use cosmic::iced::advanced::widget::{Operation, Tree, tree};
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, overlay, renderer};
// `cosmic::Element`, not iced's: the two differ in their theme, and this
// widget lives in a cosmic tree.
use cosmic::Element;
use cosmic::iced::{Event, Length, Rectangle, Size, Vector};

/// Where each row of one scroll area was last laid out, in points from the top
/// of the content, beside how tall it is.
///
/// Shared with whoever wants to scroll: the widget writes during layout, the
/// app reads when a jump asks for a row. Late answers are the point — a row
/// only just mounted is measured on the frame it appears, and the jump that
/// wanted it can ask again on the next one rather than having missed its one
/// chance.
#[derive(Clone, Default)]
pub struct Placements(Arc<Mutex<HashMap<i32, (f32, f32)>>>);

impl Placements {
    pub fn new() -> Self {
        Self::default()
    }

    /// Where row `key` sits, if it has been laid out since the last time the
    /// list it is in was.
    pub fn get(&self, key: i32) -> Option<(f32, f32)> {
        self.0.lock().ok()?.get(&key).copied()
    }

    /// How many rows have a place written down. For the log alone.
    pub fn len(&self) -> usize {
        self.0.lock().map(|held| held.len()).unwrap_or(0)
    }

    fn write(&self, rows: HashMap<i32, (f32, f32)>) {
        if let Ok(mut held) = self.0.lock() {
            *held = rows;
        }
    }
}

/// The trivial state that gives this widget a type of its own.
///
/// It holds nothing. What it is for is the tag: a wrapper with no state is
/// indistinguishable from a plain column to iced's diffing, which reuses the
/// tree it kept for one as the tree for the other — and the mismatch is not
/// noticed until something deep inside reads a child that was never there.
struct State;

pub struct Rows<'a, Message> {
    inner: Element<'a, Message>,
    keys: Vec<i32>,
    placements: Placements,
}

impl<'a, Message> Rows<'a, Message> {
    /// `keys` are the rows of `inner`, in the order they were given to it.
    pub fn new(
        inner: impl Into<Element<'a, Message>>,
        keys: Vec<i32>,
        placements: Placements,
    ) -> Self {
        Self {
            inner: inner.into(),
            keys,
            placements,
        }
    }
}

impl<Message> Widget<Message, cosmic::Theme, cosmic::Renderer> for Rows<'_, Message> {
    fn tag(&self) -> tree::Tag {
        tree::Tag::of::<State>()
    }

    fn state(&self) -> tree::State {
        tree::State::new(State)
    }

    fn children(&self) -> Vec<Tree> {
        vec![Tree::new(&self.inner)]
    }

    fn diff(&mut self, tree: &mut Tree) {
        tree.diff_children(std::slice::from_mut(&mut self.inner));
    }

    fn size(&self) -> Size<Length> {
        self.inner.as_widget().size()
    }

    fn layout(
        &mut self,
        tree: &mut Tree,
        renderer: &cosmic::Renderer,
        limits: &layout::Limits,
    ) -> layout::Node {
        let node = self
            .inner
            .as_widget_mut()
            .layout(&mut tree.children[0], renderer, limits);

        // The column's own children, in the order its keys were given. Their
        // bounds are already relative to the content's top, which is the
        // coordinate `scroll_to` is asking for.
        //
        // Written whole rather than merged: a row that has left the list has
        // no place any more, and an old answer for it would send a jump to
        // wherever it used to be.
        let mut rows = HashMap::with_capacity(self.keys.len());
        for (key, child) in self.keys.iter().zip(node.children()) {
            let bounds = child.bounds();
            rows.insert(*key, (bounds.y, bounds.height));
        }
        self.placements.write(rows);

        let size = node.size();
        layout::Node::with_children(size, vec![node])
    }

    fn operate(
        &mut self,
        tree: &mut Tree,
        layout: Layout<'_>,
        renderer: &cosmic::Renderer,
        operation: &mut dyn Operation,
    ) {
        operation.traverse(&mut |operation| {
            self.inner.as_widget_mut().operate(
                &mut tree.children[0],
                inner_layout(layout),
                renderer,
                operation,
            );
        });
    }

    fn update(
        &mut self,
        tree: &mut Tree,
        event: &Event,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        renderer: &cosmic::Renderer,
        clipboard: &mut dyn Clipboard,
        shell: &mut Shell<'_, Message>,
        viewport: &Rectangle,
    ) {
        self.inner.as_widget_mut().update(
            &mut tree.children[0],
            event,
            inner_layout(layout),
            cursor,
            renderer,
            clipboard,
            shell,
            viewport,
        );
    }

    fn mouse_interaction(
        &self,
        tree: &Tree,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        viewport: &Rectangle,
        renderer: &cosmic::Renderer,
    ) -> mouse::Interaction {
        self.inner.as_widget().mouse_interaction(
            &tree.children[0],
            inner_layout(layout),
            cursor,
            viewport,
            renderer,
        )
    }

    fn draw(
        &self,
        tree: &Tree,
        renderer: &mut cosmic::Renderer,
        theme: &cosmic::Theme,
        style: &renderer::Style,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        viewport: &Rectangle,
    ) {
        self.inner.as_widget().draw(
            &tree.children[0],
            renderer,
            theme,
            style,
            inner_layout(layout),
            cursor,
            viewport,
        );
    }

    fn overlay<'b>(
        &'b mut self,
        tree: &'b mut Tree,
        layout: Layout<'b>,
        renderer: &cosmic::Renderer,
        viewport: &Rectangle,
        translation: Vector,
    ) -> Option<overlay::Element<'b, Message, cosmic::Theme, cosmic::Renderer>> {
        self.inner.as_widget_mut().overlay(
            &mut tree.children[0],
            inner_layout(layout),
            renderer,
            viewport,
            translation,
        )
    }
}

/// The one child this widget's layout node has.
fn inner_layout(layout: Layout<'_>) -> Layout<'_> {
    layout
        .children()
        .next()
        .expect("a Rows layout holds exactly one child")
}

impl<'a, Message: 'a> From<Rows<'a, Message>> for Element<'a, Message> {
    fn from(rows: Rows<'a, Message>) -> Self {
        Element::new(rows)
    }
}