Keep asking until the row is there, and say so when it is not
A jump asks on the commit the reader made it, which is a frame before the room it lands in has been built: no row is laid out, so no row has a place written down, so there is nothing to scroll to yet. The ask is answered by trying again on the next frame, and the next — but it used to stop the moment nothing was asking, and a room part-way through being built looks exactly like a jump that is over. It is not over, it is early, so the budget is spent in full. Which row is asking is asked again each time rather than carried. Rows are rebuilt under the reconciler as a buffer takes lines, so the row that was node 412 a frame ago is node 587 now, and a retry holding the old number looks up a place for a row nobody has. Which room a reader jumped into decided whether the jump worked, which is as strange as it sounds until you see what it hung on. And `JOLT_SCROLL_LOG` says what each jump decided: the row, where the layout put it, the viewport it is centred in, and the offset asked for — or that it has no place yet and how many tries are left. A jump is three numbers and a lookup, and which of them is wrong is not a thing anyone can tell from a window that scrolled to the wrong place. Two bugs were found with it that were not going to be found by reading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b49f82a parent: 0ab003e modified
crates/jolt-cosmic/src/lib.rs +149 -7 | @@ -355,6 +355,104 @@ fn centred_offset(top: f32, height: f32, viewport: f32) -> f32 { | ||
| 355 | 355 | (top - (viewport - height).max(0.0) / 2.0).max(0.0) |
| 356 | 356 | } |
| 357 | 357 | |
| 358 | +/// Whether to say out loud what every jump decided, on stderr. | |
| 359 | +/// | |
| 360 | +/// Set `JOLT_SCROLL_LOG` to anything. A jump is three numbers and a lookup, | |
| 361 | +/// and which of them is wrong is not a thing anyone can tell from a window | |
| 362 | +/// that scrolled to the wrong place. | |
| 363 | +fn scroll_log() -> bool { | |
| 364 | + static ON: LazyLock<bool> = LazyLock::new(|| std::env::var_os("JOLT_SCROLL_LOG").is_some()); | |
| 365 | + *ON | |
| 366 | +} | |
| 367 | + | |
| 368 | +/// How many frames a reveal keeps trying for. | |
| 369 | +/// | |
| 370 | +/// A row is measured by the layout that draws it, so the frame a jump is asked | |
| 371 | +/// on is a frame too early: the places written down are the ones from before | |
| 372 | +/// the room changed. Twenty frames is a third of a second at sixty, which is | |
| 373 | +/// longer than a screen takes to build and shorter than a reader waits before | |
| 374 | +/// deciding nothing happened. | |
| 375 | +const REVEAL_TRIES: u8 = 20; | |
| 376 | + | |
| 377 | +/// The row of the scroll area called `name` that is asking to be shown, as the | |
| 378 | +/// tree has it now. | |
| 379 | +/// | |
| 380 | +/// Asked again on every attempt rather than carried, because a row is not the | |
| 381 | +/// same node for long. A buffer that takes a line while a jump is landing is | |
| 382 | +/// rebuilt under the reconciler, and the row that was node 412 a frame ago is | |
| 383 | +/// node 587 now — so a retry holding the old number would look up a place for | |
| 384 | +/// a row nobody has, and go on failing until it gave up. Which room a reader | |
| 385 | +/// jumped into decided whether it worked, and that is exactly as strange as | |
| 386 | +/// it sounds until you see what it depends on. | |
| 387 | +fn asking_row(t: &Tree, name: &str) -> Option<i32> { | |
| 388 | + let mut found = None; | |
| 389 | + walk(t, t.root_id(), &mut |id, n| { | |
| 390 | + if found.is_some() || n.tag != "scroll" || scroll_name(n, id) != name { | |
| 391 | + return; | |
| 392 | + } | |
| 393 | + for row in &n.children { | |
| 394 | + let mut asked = false; | |
| 395 | + walk(t, *row, &mut |_, node| { | |
| 396 | + asked |= node.bool("scroll-here") == Some(true); | |
| 397 | + }); | |
| 398 | + if asked { | |
| 399 | + found = Some(*row); | |
| 400 | + break; | |
| 401 | + } | |
| 402 | + } | |
| 403 | + }); | |
| 404 | + found | |
| 405 | +} | |
| 406 | + | |
| 407 | +/// Ask to be taken to the row of `name` that wants showing — now if its place | |
| 408 | +/// is known, and on the next frame if it is not. | |
| 409 | +/// | |
| 410 | +/// A task that is already finished is not a wasted frame: iced takes its | |
| 411 | +/// message on the next pass of the loop, which is after this frame has been | |
| 412 | +/// laid out — and being laid out is exactly what the row has to have done for | |
| 413 | +/// there to be an answer. | |
| 414 | +fn reveal(name: String, row: i32, viewport: f32) -> Task<Message> { | |
| 415 | + match placements(&name).get(row) { | |
| 416 | + Some((top, height)) => { | |
| 417 | + let y = centred_offset(top, height, viewport); | |
| 418 | + if scroll_log() { | |
| 419 | + eprintln!( | |
| 420 | + "jolt-scroll: {name} row {row} at {top} (h {height}), viewport {viewport} -> {y}" | |
| 421 | + ); | |
| 422 | + } | |
| 423 | + iced_scrollable::scroll_to(scroll_id(&name), AbsoluteOffset { x: None, y: Some(y) }) | |
| 424 | + } | |
| 425 | + None => { | |
| 426 | + if scroll_log() { | |
| 427 | + eprintln!("jolt-scroll: {name} row {row} has no place yet, trying again"); | |
| 428 | + } | |
| 429 | + Task::future(async move { cosmic::Action::App(Message::Reveal(name, REVEAL_TRIES)) }) | |
| 430 | + } | |
| 431 | + } | |
| 432 | +} | |
| 433 | + | |
| 434 | +/// How many rows the scroll area called `name` has, and how many of them are | |
| 435 | +/// asking to be shown. For the log alone. | |
| 436 | +fn scroll_shape(t: &Tree, name: &str) -> (usize, usize) { | |
| 437 | + let mut shape = (0, 0); | |
| 438 | + walk(t, t.root_id(), &mut |id, n| { | |
| 439 | + if shape.0 > 0 || n.tag != "scroll" || scroll_name(n, id) != name { | |
| 440 | + return; | |
| 441 | + } | |
| 442 | + shape.0 = n.children.len(); | |
| 443 | + for row in &n.children { | |
| 444 | + let mut asked = false; | |
| 445 | + walk(t, *row, &mut |_, node| { | |
| 446 | + asked |= node.bool("scroll-here") == Some(true); | |
| 447 | + }); | |
| 448 | + if asked { | |
| 449 | + shape.1 += 1; | |
| 450 | + } | |
| 451 | + } | |
| 452 | + }); | |
| 453 | + shape | |
| 454 | +} | |
| 455 | + | |
| 358 | 456 | fn snap_to_end(name: &str) -> Task<Message> { |
| 359 | 457 | iced_scrollable::snap_to(scroll_id(name), RelativeOffset { x: None, y: Some(1.0) }) |
| 360 | 458 | } |
| @@ -383,6 +481,9 @@ enum Message { | ||
| 383 | 481 | Hover(i32), |
| 384 | 482 | Unhover(i32), |
| 385 | 483 | Scrolled(i32, String, Viewport), |
| 484 | + /// Show whichever row of this scroll area is asking to be shown, and how | |
| 485 | + /// many more frames to keep trying for. See `reveal`. | |
| 486 | + Reveal(String, u8), | |
| 386 | 487 | PickImage, |
| 387 | 488 | Picked(Option<PathBuf>), |
| 388 | 489 | } |
| @@ -449,14 +550,15 @@ impl App { | ||
| 449 | 550 | // last layout. Both, or there is nothing to do yet: the row is |
| 450 | 551 | // measured on the frame it appears, and the ask stands until it |
| 451 | 552 | // has been. |
| 452 | - if let Some((top, height)) = ask.reveal.and_then(|row| placements(&ask.name).get(row)) { | |
| 453 | - let y = centred_offset(top, height, memo.height); | |
| 553 | + // A row is asking to be shown. Whether or not it can be shown yet, | |
| 554 | + // nothing else may move this list while it is asking: the branch | |
| 555 | + // below would otherwise take a reader who was at the newest line — | |
| 556 | + // which is most readers, most of the time — straight back to it, | |
| 557 | + // and a jump that ends at the bottom of the room reads as a jump | |
| 558 | + // that did nothing. | |
| 559 | + if let Some(row) = ask.reveal { | |
| 454 | 560 | memo.at_end = false; |
| 455 | - memo.offset_y = y; | |
| 456 | - tasks.push(iced_scrollable::scroll_to( | |
| 457 | - scroll_id(&ask.name), | |
| 458 | - AbsoluteOffset { x: None, y: Some(y) }, | |
| 459 | - )); | |
| 561 | + tasks.push(reveal(ask.name.clone(), row, memo.height)); | |
| 460 | 562 | } else if jumped || (ask.stick && memo.at_end) { |
| 461 | 563 | memo.at_end = true; |
| 462 | 564 | tasks.push(snap_to_end(&ask.name)); |
| @@ -568,6 +670,46 @@ impl cosmic::Application for App { | ||
| 568 | 670 | Message::Hover(node) => post(node, "hover", String::new(), 0.0), |
| 569 | 671 | Message::Unhover(node) => post(node, "unhover", String::new(), 0.0), |
| 570 | 672 | Message::Scrolled(node, name, viewport) => self.scrolled(node, name, viewport), |
| 673 | + // The row was not laid out when the jump was asked for. Look | |
| 674 | + // again, and keep looking for a few frames: a room the reader has | |
| 675 | + // only just been taken to has to be built before its lines have | |
| 676 | + // anywhere to be. | |
| 677 | + Message::Reveal(name, tries) => { | |
| 678 | + let viewport = self.scrolls.get(&name).map_or(0.0, |memo| memo.height); | |
| 679 | + let place = asking_row(&self.tree, &name) | |
| 680 | + .and_then(|row| placements(&name).get(row)); | |
| 681 | + if let Some((top, height)) = place { | |
| 682 | + let y = centred_offset(top, height, viewport); | |
| 683 | + if let Some(memo) = self.scrolls.get_mut(&name) { | |
| 684 | + memo.at_end = false; | |
| 685 | + memo.offset_y = y; | |
| 686 | + } | |
| 687 | + return iced_scrollable::scroll_to( | |
| 688 | + scroll_id(&name), | |
| 689 | + AbsoluteOffset { x: None, y: Some(y) }, | |
| 690 | + ); | |
| 691 | + } | |
| 692 | + if scroll_log() { | |
| 693 | + let asking = asking_row(&self.tree, &name); | |
| 694 | + let (rows, here) = scroll_shape(&self.tree, &name); | |
| 695 | + eprintln!( | |
| 696 | + "jolt-scroll: {name} retry {tries}, asking {asking:?}, \ | |
| 697 | + {rows} rows, {here} asking to be shown, \ | |
| 698 | + {} placed, viewport {viewport}", | |
| 699 | + placements(&name).len() | |
| 700 | + ); | |
| 701 | + } | |
| 702 | + // Not landed yet. Keep trying for the whole budget rather | |
| 703 | + // than stopping the moment nothing is asking: a room the | |
| 704 | + // reader has just been taken to is built over several frames, | |
| 705 | + // and one where the rows are not in the tree yet looks exactly | |
| 706 | + // like a jump that is over. It is not over, it is early. | |
| 707 | + if tries > 0 { | |
| 708 | + return Task::future(async move { | |
| 709 | + cosmic::Action::App(Message::Reveal(name, tries - 1)) | |
| 710 | + }); | |
| 711 | + } | |
| 712 | + } | |
| 571 | 713 | // The desktop's own chooser, through the portal, on libcosmic's |
| 572 | 714 | // executor: it is a D-Bus round trip, and the window keeps |
| 573 | 715 | // painting while it is open. |
| @@ -355,6 +355,104 @@ fn centred_offset(top: f32, height: f32, viewport: f32) -> f32 { | |||
| 355 | (top - (viewport - height).max(0.0) / 2.0).max(0.0) | 355 | (top - (viewport - height).max(0.0) / 2.0).max(0.0) |
| 356 | } | 356 | } |
| 357 | 357 | ||
| 358 | +/// Whether to say out loud what every jump decided, on stderr. | ||
| 359 | +/// | ||
| 360 | +/// Set `JOLT_SCROLL_LOG` to anything. A jump is three numbers and a lookup, | ||
| 361 | +/// and which of them is wrong is not a thing anyone can tell from a window | ||
| 362 | +/// that scrolled to the wrong place. | ||
| 363 | +fn scroll_log() -> bool { | ||
| 364 | + static ON: LazyLock<bool> = LazyLock::new(|| std::env::var_os("JOLT_SCROLL_LOG").is_some()); | ||
| 365 | + *ON | ||
| 366 | +} | ||
| 367 | + | ||
| 368 | +/// How many frames a reveal keeps trying for. | ||
| 369 | +/// | ||
| 370 | +/// A row is measured by the layout that draws it, so the frame a jump is asked | ||
| 371 | +/// on is a frame too early: the places written down are the ones from before | ||
| 372 | +/// the room changed. Twenty frames is a third of a second at sixty, which is | ||
| 373 | +/// longer than a screen takes to build and shorter than a reader waits before | ||
| 374 | +/// deciding nothing happened. | ||
| 375 | +const REVEAL_TRIES: u8 = 20; | ||
| 376 | + | ||
| 377 | +/// The row of the scroll area called `name` that is asking to be shown, as the | ||
| 378 | +/// tree has it now. | ||
| 379 | +/// | ||
| 380 | +/// Asked again on every attempt rather than carried, because a row is not the | ||
| 381 | +/// same node for long. A buffer that takes a line while a jump is landing is | ||
| 382 | +/// rebuilt under the reconciler, and the row that was node 412 a frame ago is | ||
| 383 | +/// node 587 now — so a retry holding the old number would look up a place for | ||
| 384 | +/// a row nobody has, and go on failing until it gave up. Which room a reader | ||
| 385 | +/// jumped into decided whether it worked, and that is exactly as strange as | ||
| 386 | +/// it sounds until you see what it depends on. | ||
| 387 | +fn asking_row(t: &Tree, name: &str) -> Option<i32> { | ||
| 388 | + let mut found = None; | ||
| 389 | + walk(t, t.root_id(), &mut |id, n| { | ||
| 390 | + if found.is_some() || n.tag != "scroll" || scroll_name(n, id) != name { | ||
| 391 | + return; | ||
| 392 | + } | ||
| 393 | + for row in &n.children { | ||
| 394 | + let mut asked = false; | ||
| 395 | + walk(t, *row, &mut |_, node| { | ||
| 396 | + asked |= node.bool("scroll-here") == Some(true); | ||
| 397 | + }); | ||
| 398 | + if asked { | ||
| 399 | + found = Some(*row); | ||
| 400 | + break; | ||
| 401 | + } | ||
| 402 | + } | ||
| 403 | + }); | ||
| 404 | + found | ||
| 405 | +} | ||
| 406 | + | ||
| 407 | +/// Ask to be taken to the row of `name` that wants showing — now if its place | ||
| 408 | +/// is known, and on the next frame if it is not. | ||
| 409 | +/// | ||
| 410 | +/// A task that is already finished is not a wasted frame: iced takes its | ||
| 411 | +/// message on the next pass of the loop, which is after this frame has been | ||
| 412 | +/// laid out — and being laid out is exactly what the row has to have done for | ||
| 413 | +/// there to be an answer. | ||
| 414 | +fn reveal(name: String, row: i32, viewport: f32) -> Task<Message> { | ||
| 415 | + match placements(&name).get(row) { | ||
| 416 | + Some((top, height)) => { | ||
| 417 | + let y = centred_offset(top, height, viewport); | ||
| 418 | + if scroll_log() { | ||
| 419 | + eprintln!( | ||
| 420 | + "jolt-scroll: {name} row {row} at {top} (h {height}), viewport {viewport} -> {y}" | ||
| 421 | + ); | ||
| 422 | + } | ||
| 423 | + iced_scrollable::scroll_to(scroll_id(&name), AbsoluteOffset { x: None, y: Some(y) }) | ||
| 424 | + } | ||
| 425 | + None => { | ||
| 426 | + if scroll_log() { | ||
| 427 | + eprintln!("jolt-scroll: {name} row {row} has no place yet, trying again"); | ||
| 428 | + } | ||
| 429 | + Task::future(async move { cosmic::Action::App(Message::Reveal(name, REVEAL_TRIES)) }) | ||
| 430 | + } | ||
| 431 | + } | ||
| 432 | +} | ||
| 433 | + | ||
| 434 | +/// How many rows the scroll area called `name` has, and how many of them are | ||
| 435 | +/// asking to be shown. For the log alone. | ||
| 436 | +fn scroll_shape(t: &Tree, name: &str) -> (usize, usize) { | ||
| 437 | + let mut shape = (0, 0); | ||
| 438 | + walk(t, t.root_id(), &mut |id, n| { | ||
| 439 | + if shape.0 > 0 || n.tag != "scroll" || scroll_name(n, id) != name { | ||
| 440 | + return; | ||
| 441 | + } | ||
| 442 | + shape.0 = n.children.len(); | ||
| 443 | + for row in &n.children { | ||
| 444 | + let mut asked = false; | ||
| 445 | + walk(t, *row, &mut |_, node| { | ||
| 446 | + asked |= node.bool("scroll-here") == Some(true); | ||
| 447 | + }); | ||
| 448 | + if asked { | ||
| 449 | + shape.1 += 1; | ||
| 450 | + } | ||
| 451 | + } | ||
| 452 | + }); | ||
| 453 | + shape | ||
| 454 | +} | ||
| 455 | + | ||
| 358 | fn snap_to_end(name: &str) -> Task<Message> { | 456 | fn snap_to_end(name: &str) -> Task<Message> { |
| 359 | iced_scrollable::snap_to(scroll_id(name), RelativeOffset { x: None, y: Some(1.0) }) | 457 | iced_scrollable::snap_to(scroll_id(name), RelativeOffset { x: None, y: Some(1.0) }) |
| 360 | } | 458 | } |
| @@ -383,6 +481,9 @@ enum Message { | |||
| 383 | Hover(i32), | 481 | Hover(i32), |
| 384 | Unhover(i32), | 482 | Unhover(i32), |
| 385 | Scrolled(i32, String, Viewport), | 483 | Scrolled(i32, String, Viewport), |
| 484 | + /// Show whichever row of this scroll area is asking to be shown, and how | ||
| 485 | + /// many more frames to keep trying for. See `reveal`. | ||
| 486 | + Reveal(String, u8), | ||
| 386 | PickImage, | 487 | PickImage, |
| 387 | Picked(Option<PathBuf>), | 488 | Picked(Option<PathBuf>), |
| 388 | } | 489 | } |
| @@ -449,14 +550,15 @@ impl App { | |||
| 449 | // last layout. Both, or there is nothing to do yet: the row is | 550 | // last layout. Both, or there is nothing to do yet: the row is |
| 450 | // measured on the frame it appears, and the ask stands until it | 551 | // measured on the frame it appears, and the ask stands until it |
| 451 | // has been. | 552 | // has been. |
| 452 | - if let Some((top, height)) = ask.reveal.and_then(|row| placements(&ask.name).get(row)) { | 553 | + // A row is asking to be shown. Whether or not it can be shown yet, |
| 453 | - let y = centred_offset(top, height, memo.height); | 554 | + // nothing else may move this list while it is asking: the branch |
| 555 | + // below would otherwise take a reader who was at the newest line — | ||
| 556 | + // which is most readers, most of the time — straight back to it, | ||
| 557 | + // and a jump that ends at the bottom of the room reads as a jump | ||
| 558 | + // that did nothing. | ||
| 559 | + if let Some(row) = ask.reveal { | ||
| 454 | memo.at_end = false; | 560 | memo.at_end = false; |
| 455 | - memo.offset_y = y; | 561 | + tasks.push(reveal(ask.name.clone(), row, memo.height)); |
| 456 | - tasks.push(iced_scrollable::scroll_to( | ||
| 457 | - scroll_id(&ask.name), | ||
| 458 | - AbsoluteOffset { x: None, y: Some(y) }, | ||
| 459 | - )); | ||
| 460 | } else if jumped || (ask.stick && memo.at_end) { | 562 | } else if jumped || (ask.stick && memo.at_end) { |
| 461 | memo.at_end = true; | 563 | memo.at_end = true; |
| 462 | tasks.push(snap_to_end(&ask.name)); | 564 | tasks.push(snap_to_end(&ask.name)); |
| @@ -568,6 +670,46 @@ impl cosmic::Application for App { | |||
| 568 | Message::Hover(node) => post(node, "hover", String::new(), 0.0), | 670 | Message::Hover(node) => post(node, "hover", String::new(), 0.0), |
| 569 | Message::Unhover(node) => post(node, "unhover", String::new(), 0.0), | 671 | Message::Unhover(node) => post(node, "unhover", String::new(), 0.0), |
| 570 | Message::Scrolled(node, name, viewport) => self.scrolled(node, name, viewport), | 672 | Message::Scrolled(node, name, viewport) => self.scrolled(node, name, viewport), |
| 673 | + // The row was not laid out when the jump was asked for. Look | ||
| 674 | + // again, and keep looking for a few frames: a room the reader has | ||
| 675 | + // only just been taken to has to be built before its lines have | ||
| 676 | + // anywhere to be. | ||
| 677 | + Message::Reveal(name, tries) => { | ||
| 678 | + let viewport = self.scrolls.get(&name).map_or(0.0, |memo| memo.height); | ||
| 679 | + let place = asking_row(&self.tree, &name) | ||
| 680 | + .and_then(|row| placements(&name).get(row)); | ||
| 681 | + if let Some((top, height)) = place { | ||
| 682 | + let y = centred_offset(top, height, viewport); | ||
| 683 | + if let Some(memo) = self.scrolls.get_mut(&name) { | ||
| 684 | + memo.at_end = false; | ||
| 685 | + memo.offset_y = y; | ||
| 686 | + } | ||
| 687 | + return iced_scrollable::scroll_to( | ||
| 688 | + scroll_id(&name), | ||
| 689 | + AbsoluteOffset { x: None, y: Some(y) }, | ||
| 690 | + ); | ||
| 691 | + } | ||
| 692 | + if scroll_log() { | ||
| 693 | + let asking = asking_row(&self.tree, &name); | ||
| 694 | + let (rows, here) = scroll_shape(&self.tree, &name); | ||
| 695 | + eprintln!( | ||
| 696 | + "jolt-scroll: {name} retry {tries}, asking {asking:?}, \ | ||
| 697 | + {rows} rows, {here} asking to be shown, \ | ||
| 698 | + {} placed, viewport {viewport}", | ||
| 699 | + placements(&name).len() | ||
| 700 | + ); | ||
| 701 | + } | ||
| 702 | + // Not landed yet. Keep trying for the whole budget rather | ||
| 703 | + // than stopping the moment nothing is asking: a room the | ||
| 704 | + // reader has just been taken to is built over several frames, | ||
| 705 | + // and one where the rows are not in the tree yet looks exactly | ||
| 706 | + // like a jump that is over. It is not over, it is early. | ||
| 707 | + if tries > 0 { | ||
| 708 | + return Task::future(async move { | ||
| 709 | + cosmic::Action::App(Message::Reveal(name, tries - 1)) | ||
| 710 | + }); | ||
| 711 | + } | ||
| 712 | + } | ||
| 571 | // The desktop's own chooser, through the portal, on libcosmic's | 713 | // The desktop's own chooser, through the portal, on libcosmic's |
| 572 | // executor: it is a D-Bus round trip, and the window keeps | 714 | // executor: it is a D-Bus round trip, and the window keeps |
| 573 | // painting while it is open. | 715 | // painting while it is open. |
modified
crates/jolt-cosmic/src/rows.rs +5 -0 | @@ -51,6 +51,11 @@ impl Placements { | ||
| 51 | 51 | self.0.lock().ok()?.get(&key).copied() |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | + /// How many rows have a place written down. For the log alone. | |
| 55 | + pub fn len(&self) -> usize { | |
| 56 | + self.0.lock().map(|held| held.len()).unwrap_or(0) | |
| 57 | + } | |
| 58 | + | |
| 54 | 59 | fn write(&self, rows: HashMap<i32, (f32, f32)>) { |
| 55 | 60 | if let Ok(mut held) = self.0.lock() { |
| 56 | 61 | *held = rows; |
| @@ -51,6 +51,11 @@ impl Placements { | |||
| 51 | self.0.lock().ok()?.get(&key).copied() | 51 | self.0.lock().ok()?.get(&key).copied() |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | + /// How many rows have a place written down. For the log alone. | ||
| 55 | + pub fn len(&self) -> usize { | ||
| 56 | + self.0.lock().map(|held| held.len()).unwrap_or(0) | ||
| 57 | + } | ||
| 58 | + | ||
| 54 | fn write(&self, rows: HashMap<i32, (f32, f32)>) { | 59 | fn write(&self, rows: HashMap<i32, (f32, f32)>) { |
| 55 | if let Ok(mut held) = self.0.lock() { | 60 | if let Ok(mut held) = self.0.lock() { |
| 56 | *held = rows; | 61 | *held = rows; |