| @@ -36,8 +36,6 @@ use cosmic::iced::futures::{Stream, StreamExt}; |
| 36 | use cosmic::iced::widget::container::Style as ContainerStyle; | 36 | use cosmic::iced::widget::container::Style as ContainerStyle; |
| 37 | use cosmic::iced::widget::scrollable::{self as iced_scrollable, AbsoluteOffset, RelativeOffset, Viewport}; | 37 | use cosmic::iced::widget::scrollable::{self as iced_scrollable, AbsoluteOffset, RelativeOffset, Viewport}; |
| 38 | use cosmic::iced::widget::text::Wrapping; | 38 | use cosmic::iced::widget::text::Wrapping; |
| 39 | -use cosmic::iced::advanced::widget as advanced; | | |
| 40 | -use cosmic::iced::advanced::widget::operation::scrollable as iced_operation; | | |
| 41 | use cosmic::iced::{Alignment, Background, Border, Color, ContentFit, Font, Length, Padding, Subscription}; | 39 | use cosmic::iced::{Alignment, Background, Border, Color, ContentFit, Font, Length, Padding, Subscription}; |
| 42 | use cosmic::widget::{self, Column, Row}; | 40 | use cosmic::widget::{self, Column, Row}; |
| 43 | use cosmic::{ApplicationExt, Element}; | 41 | use cosmic::{ApplicationExt, Element}; |
| @@ -265,8 +263,9 @@ struct ScrollAsk { |
| 265 | tick_before: Option<f64>, | 263 | tick_before: Option<f64>, |
| 266 | /// Not in the tree before this commit: mounted, or mounted again. | 264 | /// Not in the tree before this commit: mounted, or mounted again. |
| 267 | fresh: bool, | 265 | fresh: bool, |
| 268 | - /// The node asking to be shown, if one is. | 266 | + /// Where, as a fraction of the list, a node that has just been asked to be |
| 269 | - reveal: Option<i32>, | 267 | + /// shown sits. |
| | 268 | + reveal: Option<f32>, |
| 270 | } | 269 | } |
| 271 | | 270 | |
| 272 | /// Every scroll area in `now`, and what changed about each since `before`. | 271 | /// Every scroll area in `now`, and what changed about each since `before`. |
| @@ -284,28 +283,23 @@ fn scroll_asks(before: &Tree, now: &Tree) -> Vec<ScrollAsk> { |
| 284 | return; | 283 | return; |
| 285 | } | 284 | } |
| 286 | let name = scroll_name(n, id); | 285 | let name = scroll_name(n, id); |
| 287 | - // The ROW holding whatever asked to be shown. The row and not the | 286 | + // A row asking to be shown, that was not asking last commit. The |
| 288 | - // asking node itself, because the row is what carries an id — see | 287 | + // position is the index of the top-level row holding it: exact for a |
| 289 | - // `element` — and the id is how the layout is asked where it is. | 288 | + // list of rows the same height and close for frq's backlog, and there |
| 290 | - // | 289 | + // is no layout to ask from here. |
| 291 | - // This used to answer with the row's index over the row count, which | 290 | + let count = n.children.len(); |
| 292 | - // is a fraction of the SCROLL RANGE rather than of the content — the | | |
| 293 | - // two agree only when the viewport is exactly one row tall — and it | | |
| 294 | - // assumed every row the same height besides, in a backlog that puts a | | |
| 295 | - // one-line message next to a picture. The landing was out by up to a | | |
| 296 | - // viewport, worst in the middle of a list. | | |
| 297 | - // | | |
| 298 | - // While it is asking, not only on the commit it starts: a row that is | | |
| 299 | - // not in the tree yet when the ask arrives would otherwise never be | | |
| 300 | - // scrolled to at all, and the ask is over in half a second. | | |
| 301 | let mut reveal = None; | 291 | let mut reveal = None; |
| 302 | - for row in &n.children { | 292 | + for (index, row) in n.children.iter().enumerate() { |
| 303 | let mut asked = false; | 293 | let mut asked = false; |
| 304 | - walk(now, *row, &mut |_, node| { | 294 | + walk(now, *row, &mut |nid, node| { |
| 305 | - asked |= node.bool("scroll-here") == Some(true); | 295 | + let here = node.bool("scroll-here") == Some(true); |
| | 296 | + let was = before |
| | 297 | + .get(nid) |
| | 298 | + .is_some_and(|old| old.bool("scroll-here") == Some(true)); |
| | 299 | + asked |= here && !was; |
| 306 | }); | 300 | }); |
| 307 | if asked { | 301 | if asked { |
| 308 | - reveal = Some(*row); | 302 | + reveal = Some(index as f32 / (count.saturating_sub(1).max(1)) as f32); |
| 309 | break; | 303 | break; |
| 310 | } | 304 | } |
| 311 | } | 305 | } |
| @@ -325,88 +319,6 @@ fn snap_to_end(name: &str) -> Task<Message> { |
| 325 | iced_scrollable::snap_to(scroll_id(name), RelativeOffset { x: None, y: Some(1.0) }) | 319 | iced_scrollable::snap_to(scroll_id(name), RelativeOffset { x: None, y: Some(1.0) }) |
| 326 | } | 320 | } |
| 327 | | 321 | |
| 328 | -/// What a row that can be scrolled to is known by. | | |
| 329 | -fn here_id(node: i32) -> widget::Id { | | |
| 330 | - widget::Id::new(format!("jolt-here-{node}")) | | |
| 331 | -} | | |
| 332 | - | | |
| 333 | -/// Where `node` sits inside the scroll area called `name`, measured, and then | | |
| 334 | -/// the scroll put there. | | |
| 335 | -/// | | |
| 336 | -/// Two steps because the answer is not in the tree. The tree says which row | | |
| 337 | -/// asked; only the layout knows how far down the content it ended up, and the | | |
| 338 | -/// layout is a thing you can ask questions of exactly once a frame, through an | | |
| 339 | -/// `Operation`. So the operation reads the row's bounds and the scroll area's, | | |
| 340 | -/// works the offset out, and hands it back as a message that scrolls there. | | |
| 341 | -/// | | |
| 342 | -/// The row is put in the MIDDLE of the viewport rather than against its top | | |
| 343 | -/// edge. A line answered three days ago is read with what was said around it, | | |
| 344 | -/// and a jump that pins it to the ceiling shows only what came after. | | |
| 345 | -fn reveal(name: String, node: i32) -> Task<Message> { | | |
| 346 | - let scroll = scroll_id(&name); | | |
| 347 | - let target = here_id(node); | | |
| 348 | - | | |
| 349 | - struct Measure { | | |
| 350 | - scroll: widget::Id, | | |
| 351 | - target: widget::Id, | | |
| 352 | - /// The scroll area: where its viewport starts, how tall it is, and | | |
| 353 | - /// how far it is scrolled already. | | |
| 354 | - view: Option<(f32, f32, f32)>, | | |
| 355 | - /// The row: where it starts and how tall it is. | | |
| 356 | - row: Option<(f32, f32)>, | | |
| 357 | - } | | |
| 358 | - | | |
| 359 | - impl advanced::Operation<f32> for Measure { | | |
| 360 | - fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn advanced::Operation<f32>)) { | | |
| 361 | - operate(self); | | |
| 362 | - } | | |
| 363 | - | | |
| 364 | - fn container(&mut self, id: Option<&widget::Id>, bounds: cosmic::iced::Rectangle) { | | |
| 365 | - if id == Some(&self.target) { | | |
| 366 | - self.row = Some((bounds.y, bounds.height)); | | |
| 367 | - } | | |
| 368 | - } | | |
| 369 | - | | |
| 370 | - fn scrollable( | | |
| 371 | - &mut self, | | |
| 372 | - id: Option<&widget::Id>, | | |
| 373 | - bounds: cosmic::iced::Rectangle, | | |
| 374 | - _content: cosmic::iced::Rectangle, | | |
| 375 | - translation: cosmic::iced::Vector, | | |
| 376 | - _state: &mut dyn iced_operation::Scrollable, | | |
| 377 | - ) { | | |
| 378 | - if id == Some(&self.scroll) { | | |
| 379 | - self.view = Some((bounds.y, bounds.height, translation.y)); | | |
| 380 | - } | | |
| 381 | - } | | |
| 382 | - | | |
| 383 | - fn finish(&self) -> advanced::operation::Outcome<f32> { | | |
| 384 | - match (self.view, self.row) { | | |
| 385 | - // Both are in window coordinates, so the row's place in the | | |
| 386 | - // content is how far it is below the viewport's top edge plus | | |
| 387 | - // how far the viewport has already been scrolled. | | |
| 388 | - (Some((view_y, view_h, scrolled)), Some((row_y, row_h))) => { | | |
| 389 | - let top = row_y - view_y + scrolled; | | |
| 390 | - let centred = top - (view_h - row_h).max(0.0) / 2.0; | | |
| 391 | - advanced::operation::Outcome::Some(centred.max(0.0)) | | |
| 392 | - } | | |
| 393 | - // The row is not laid out yet — it has only just been mounted, | | |
| 394 | - // or it is not in this scroll area at all. Nothing to say, and | | |
| 395 | - // the next commit asks again. | | |
| 396 | - _ => advanced::operation::Outcome::None, | | |
| 397 | - } | | |
| 398 | - } | | |
| 399 | - } | | |
| 400 | - | | |
| 401 | - advanced::operate(Measure { | | |
| 402 | - scroll, | | |
| 403 | - target, | | |
| 404 | - view: None, | | |
| 405 | - row: None, | | |
| 406 | - }) | | |
| 407 | - .map(move |y| cosmic::Action::App(Message::Revealed(name.clone(), y))) | | |
| 408 | -} | | |
| 409 | - | | |
| 410 | // --- the app ----------------------------------------------------------------- | 322 | // --- the app ----------------------------------------------------------------- |
| 411 | | 323 | |
| 412 | struct App { | 324 | struct App { |
| @@ -431,9 +343,6 @@ enum Message { |
| 431 | Hover(i32), | 343 | Hover(i32), |
| 432 | Unhover(i32), | 344 | Unhover(i32), |
| 433 | Scrolled(i32, String, Viewport), | 345 | Scrolled(i32, String, Viewport), |
| 434 | - /// A scroll area, and how far down its content the row that asked to be | | |
| 435 | - /// shown was measured to be. | | |
| 436 | - Revealed(String, f32), | | |
| 437 | PickImage, | 346 | PickImage, |
| 438 | Picked(Option<PathBuf>), | 347 | Picked(Option<PathBuf>), |
| 439 | } | 348 | } |
| @@ -495,9 +404,12 @@ impl App { |
| 495 | offset_y: 0.0, | 404 | offset_y: 0.0, |
| 496 | }); | 405 | }); |
| 497 | let jumped = !ask.fresh && ask.tick.is_some() && ask.tick != ask.tick_before; | 406 | let jumped = !ask.fresh && ask.tick.is_some() && ask.tick != ask.tick_before; |
| 498 | - if let Some(node) = ask.reveal { | 407 | + if let Some(fraction) = ask.reveal { |
| 499 | memo.at_end = false; | 408 | memo.at_end = false; |
| 500 | - tasks.push(reveal(ask.name.clone(), node)); | 409 | + tasks.push(iced_scrollable::snap_to( |
| | 410 | + scroll_id(&ask.name), |
| | 411 | + RelativeOffset { x: None, y: Some(fraction) }, |
| | 412 | + )); |
| 501 | } else if jumped || (ask.stick && memo.at_end) { | 413 | } else if jumped || (ask.stick && memo.at_end) { |
| 502 | memo.at_end = true; | 414 | memo.at_end = true; |
| 503 | tasks.push(snap_to_end(&ask.name)); | 415 | tasks.push(snap_to_end(&ask.name)); |
| @@ -607,17 +519,6 @@ impl cosmic::Application for App { |
| 607 | Message::Hover(node) => post(node, "hover", String::new(), 0.0), | 519 | Message::Hover(node) => post(node, "hover", String::new(), 0.0), |
| 608 | Message::Unhover(node) => post(node, "unhover", String::new(), 0.0), | 520 | Message::Unhover(node) => post(node, "unhover", String::new(), 0.0), |
| 609 | Message::Scrolled(node, name, viewport) => self.scrolled(node, name, viewport), | 521 | Message::Scrolled(node, name, viewport) => self.scrolled(node, name, viewport), |
| 610 | - // The measurement came back: put the scroll where it says. The | | |
| 611 | - // memo is written too, so a list that is unmounted and comes back | | |
| 612 | - // opens where the jump left it rather than where it was before. | | |
| 613 | - Message::Revealed(name, y) => { | | |
| 614 | - let id = scroll_id(&name); | | |
| 615 | - if let Some(memo) = self.scrolls.get_mut(&name) { | | |
| 616 | - memo.at_end = false; | | |
| 617 | - memo.offset_y = y; | | |
| 618 | - } | | |
| 619 | - return iced_scrollable::scroll_to(id, AbsoluteOffset { x: None, y: Some(y) }); | | |
| 620 | - } | | |
| 621 | // The desktop's own chooser, through the portal, on libcosmic's | 522 | // The desktop's own chooser, through the portal, on libcosmic's |
| 622 | // executor: it is a D-Bus round trip, and the window keeps | 523 | // executor: it is a D-Bus round trip, and the window keeps |
| 623 | // painting while it is open. | 524 | // painting while it is open. |
| @@ -1184,23 +1085,6 @@ fn element(t: &Tree, id: i32, enabled: bool, in_row: bool) -> Element<'_, Messag |
| 1184 | .into(), | 1085 | .into(), |
| 1185 | }; | 1086 | }; |
| 1186 | | 1087 | |
| 1187 | - // Every row of a scroll area carries an id, so the operation in `reveal` | | |
| 1188 | - // can find out where one of them actually is. | | |
| 1189 | - // | | |
| 1190 | - // Every row, and not only the row that is asking to be shown. iced tells | | |
| 1191 | - // two widgets apart by their state's type, and a wrapper with no state of | | |
| 1192 | - // its own is indistinguishable from the column it wraps — so a wrapper | | |
| 1193 | - // that came and went as a row became the target would have the old tree | | |
| 1194 | - // reused for the new shape, and the state under it would be read as | | |
| 1195 | - // something it is not. That is not a wrong answer, it is a panic in the | | |
| 1196 | - // middle of a layout. Whether a node is a row of a scroll area is a | | |
| 1197 | - // question about where it sits, and where it sits does not change under | | |
| 1198 | - // it. | | |
| 1199 | - let el = match t.get(n.parent) { | | |
| 1200 | - Some(parent) if parent.tag == "scroll" => widget::id_container(el, here_id(id)).into(), | | |
| 1201 | - _ => el, | | |
| 1202 | - }; | | |
| 1203 | - | | |
| 1204 | // The containers and the entry size themselves above; anything else asked | 1088 | // The containers and the entry size themselves above; anything else asked |
| 1205 | // for a width gets it from a wrapper. | 1089 | // for a width gets it from a wrapper. |
| 1206 | match (n.tag.as_str(), width_request(n)) { | 1090 | match (n.tag.as_str(), width_request(n)) { |
| @@ -1637,7 +1521,7 @@ mod tests { |
| 1637 | } | 1521 | } |
| 1638 | | 1522 | |
| 1639 | #[test] | 1523 | #[test] |
| 1640 | - fn scroll_here_asks_with_the_row_itself_and_goes_on_asking() { | 1524 | + fn a_new_scroll_here_asks_for_its_row_and_a_standing_one_does_not() { |
| 1641 | let mut t = Tree::default(); | 1525 | let mut t = Tree::default(); |
| 1642 | let root = t.root(); | 1526 | let root = t.root(); |
| 1643 | let list = node(&mut t, root, "scroll"); | 1527 | let list = node(&mut t, root, "scroll"); |
| @@ -1646,33 +1530,13 @@ mod tests { |
| 1646 | let before = t.clone(); | 1530 | let before = t.clone(); |
| 1647 | t.set(rows[3], "scroll-here", Prop::Bool(true)); | 1531 | t.set(rows[3], "scroll-here", Prop::Bool(true)); |
| 1648 | | 1532 | |
| 1649 | - // The row, not a guess at where it sits: only the layout knows that, | | |
| 1650 | - // and `reveal` is what goes and asks it. Here the row and the node | | |
| 1651 | - // asking are the same; a message row asks from inside itself, and the | | |
| 1652 | - // answer is still the row, since the row is what carries an id. | | |
| 1653 | let asks = scroll_asks(&before, &t); | 1533 | let asks = scroll_asks(&before, &t); |
| 1654 | assert_eq!(asks.len(), 1); | 1534 | assert_eq!(asks.len(), 1); |
| 1655 | assert!(!asks[0].fresh); | 1535 | assert!(!asks[0].fresh); |
| 1656 | - assert_eq!(asks[0].reveal, Some(rows[3])); | 1536 | + assert_eq!(asks[0].reveal, Some(0.75)); |
| 1657 | | 1537 | |
| 1658 | - // And it asks again while the row is still asking. It used to ask only | | |
| 1659 | - // on the commit the prop arrived, which meant a row not laid out yet — | | |
| 1660 | - // a conversation the client has only just switched to — was never | | |
| 1661 | - // scrolled to at all. | | |
| 1662 | let again = scroll_asks(&t, &t); | 1538 | let again = scroll_asks(&t, &t); |
| 1663 | - assert_eq!(again[0].reveal, Some(rows[3])); | 1539 | + assert_eq!(again[0].reveal, None); |
| 1664 | - | | |
| 1665 | - // Nothing asking, nothing to reveal. | | |
| 1666 | - t.set(rows[3], "scroll-here", Prop::Bool(false)); | | |
| 1667 | - let quiet = scroll_asks(&t, &t); | | |
| 1668 | - assert_eq!(quiet[0].reveal, None); | | |
| 1669 | - | | |
| 1670 | - // And a node asking from deeper inside a row still answers with the | | |
| 1671 | - // row: that is the thing the layout can be asked about. | | |
| 1672 | - let inner = node(&mut t, rows[1], "vbox"); | | |
| 1673 | - t.set(inner, "scroll-here", Prop::Bool(true)); | | |
| 1674 | - let deep = scroll_asks(&t, &t); | | |
| 1675 | - assert_eq!(deep[0].reveal, Some(rows[1])); | | |
| 1676 | } | 1540 | } |
| 1677 | | 1541 | |
| 1678 | #[test] | 1542 | #[test] |