mirror of
https://github.com/danbulant/cushy
synced 2026-05-25 04:42:34 +00:00
Async, better scroll, Input::on_key
This commit is contained in:
parent
0026a6db0d
commit
501eecd7a5
11 changed files with 274 additions and 70 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -96,7 +96,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "appit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/appit#91c540c2a2db69eb25ea47eccb7aac1eb911933e"
|
||||
source = "git+https://github.com/khonsulabs/appit#043bfe2c78524d6a06ed159289ea1cd7a62b0fec"
|
||||
dependencies = [
|
||||
"raw-window-handle 0.5.2",
|
||||
"winit",
|
||||
|
|
@ -847,7 +847,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#35b83e14e71a02f3cd9a96e865094f5b3ad1407c"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#ce69ff4ecf5995a3120d2fc56f4fa6c16381d6b5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
@ -2428,18 +2428,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.23"
|
||||
version = "0.7.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e50cbb27c30666a6108abd6bc7577556265b44f243e2be89a8bc4e07a528c107"
|
||||
checksum = "092cd76b01a033a9965b9097da258689d9e17c69ded5dcf41bca001dd20ebc6d"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.23"
|
||||
version = "0.7.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a25f293fe55f0a48e7010d65552bb63704f6ceb55a1a385da10d41d8f78e4a3d"
|
||||
checksum = "a13a20a7c6a90e2034bcc65495799da92efcec6a8dd4f3fcb6f7a48988637ead"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ kempt = "0.2.1"
|
|||
|
||||
# [patch."https://github.com/khonsulabs/kludgine"]
|
||||
# kludgine = { path = "../kludgine2" }
|
||||
# [patch."https://github.com/khonsulabs/appit"]
|
||||
# appit = { path = "../appit" }
|
||||
# [patch."https://github.com/khonsulabs/figures"]
|
||||
# figures = { path = "../figures" }
|
||||
|
||||
|
|
|
|||
46
examples/gameui.rs
Normal file
46
examples/gameui.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::{HANDLED, IGNORED};
|
||||
use gooey::widgets::{Canvas, Expand, Input, Label, Scroll, Stack};
|
||||
use gooey::{widgets, Run};
|
||||
use kludgine::app::winit::event::ElementState;
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
use kludgine::figures::{Point, Rect};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::Color;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100));
|
||||
let chat_message = Dynamic::new(String::new());
|
||||
|
||||
Expand::new(Stack::rows(widgets![
|
||||
Expand::new(Stack::columns(widgets![
|
||||
Expand::new(Scroll::vertical(Label::new(chat_log.clone()))),
|
||||
Expand::weighted(
|
||||
2,
|
||||
Canvas::new(|context| {
|
||||
let entire_canvas = Rect::from(context.graphics.size());
|
||||
context.graphics.draw_shape(
|
||||
&Shape::filled_rect(entire_canvas, Color::RED),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
})
|
||||
)
|
||||
])),
|
||||
Input::new(chat_message.clone()).on_key(move |input| {
|
||||
match (input.state, input.logical_key) {
|
||||
(ElementState::Pressed, Key::Enter) => {
|
||||
let new_message = chat_message.map_mut(|text| std::mem::take(text));
|
||||
chat_log.map_mut(|chat_log| {
|
||||
chat_log.push_str(&new_message);
|
||||
chat_log.push('\n');
|
||||
});
|
||||
HANDLED
|
||||
}
|
||||
_ => IGNORED,
|
||||
}
|
||||
}),
|
||||
]))
|
||||
.run()
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ use alot::{LotId, Lots};
|
|||
use kempt::Set;
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::impl_all_tuples;
|
||||
use crate::value::Dynamic;
|
||||
|
||||
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
|
||||
|
|
|
|||
20
src/lib.rs
20
src/lib.rs
|
|
@ -2,6 +2,9 @@
|
|||
#![warn(clippy::pedantic, missing_docs)]
|
||||
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
|
||||
|
||||
#[macro_use]
|
||||
mod utils;
|
||||
|
||||
pub mod animation;
|
||||
pub mod context;
|
||||
mod graphics;
|
||||
|
|
@ -9,7 +12,6 @@ mod names;
|
|||
pub mod styles;
|
||||
mod tick;
|
||||
mod tree;
|
||||
mod utils;
|
||||
pub mod value;
|
||||
pub mod widget;
|
||||
pub mod widgets;
|
||||
|
|
@ -88,6 +90,9 @@ macro_rules! widgets {
|
|||
}};
|
||||
}
|
||||
|
||||
/// Counts the number of expressions passed to it.
|
||||
///
|
||||
/// This is used inside of Gooey macros to preallocate collections.
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! count {
|
||||
|
|
@ -115,16 +120,3 @@ macro_rules! styles {
|
|||
$crate::styles!($($component => $value),*)
|
||||
}};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! impl_all_tuples {
|
||||
($macro_name:ident) => {
|
||||
$macro_name!(T0 0);
|
||||
$macro_name!(T0 0, T1 1);
|
||||
$macro_name!(T0 0, T1 1, T2 2);
|
||||
$macro_name!(T0 0, T1 1, T2 2, T3 3);
|
||||
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4);
|
||||
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/utils.rs
15
src/utils.rs
|
|
@ -62,3 +62,18 @@ impl<T> Deref for Lazy<T> {
|
|||
self.once.get_or_init(self.init)
|
||||
}
|
||||
}
|
||||
|
||||
/// Invokes the provided macro with a pattern that can be matched using this
|
||||
/// macro_rules expression: `$($type:ident $field:tt),+`, where `$type` is an
|
||||
/// identifier to use for the generic parameter and `$field` is the field index
|
||||
/// inside of the tuple.
|
||||
macro_rules! impl_all_tuples {
|
||||
($macro_name:ident) => {
|
||||
$macro_name!(T0 0);
|
||||
$macro_name!(T0 0, T1 1);
|
||||
$macro_name!(T0 0, T1 1, T2 2);
|
||||
$macro_name!(T0 0, T1 1, T2 2, T3 3);
|
||||
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4);
|
||||
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
src/value.rs
81
src/value.rs
|
|
@ -1,8 +1,10 @@
|
|||
//! Types for storing and interacting with values in Widgets.
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
|
||||
use std::task::{Poll, Waker};
|
||||
|
||||
use crate::animation::{DynamicTransition, LinearInterpolate};
|
||||
use crate::context::{WidgetContext, WindowHandle};
|
||||
|
|
@ -24,6 +26,7 @@ impl<T> Dynamic<T> {
|
|||
callbacks: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
readers: 0,
|
||||
wakers: Vec::new(),
|
||||
}),
|
||||
sync: AssertUnwindSafe(Condvar::new()),
|
||||
}))
|
||||
|
|
@ -39,7 +42,7 @@ impl<T> Dynamic<T> {
|
|||
/// function, all observers will be notified that the contents have been
|
||||
/// updated.
|
||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||
self.0.map_mut(map)
|
||||
self.0.map_mut(|value, _| map(value))
|
||||
}
|
||||
|
||||
/// Attaches `for_each` to this value so that it is invoked each time the
|
||||
|
|
@ -106,7 +109,8 @@ impl<T> Dynamic<T> {
|
|||
/// the contents have been updated.
|
||||
#[must_use]
|
||||
pub fn replace(&self, new_value: T) -> T {
|
||||
self.0.map_mut(|value| std::mem::replace(value, new_value))
|
||||
self.0
|
||||
.map_mut(|value, _| std::mem::replace(value, new_value))
|
||||
}
|
||||
|
||||
/// Stores `new_value` in this dynamic. Before returning from this function,
|
||||
|
|
@ -115,6 +119,21 @@ impl<T> Dynamic<T> {
|
|||
let _old = self.replace(new_value);
|
||||
}
|
||||
|
||||
/// Updates this dynamic with `new_value`, but only if `new_value` is not
|
||||
/// equal to the currently stored value.
|
||||
pub fn update(&self, new_value: T)
|
||||
where
|
||||
T: Eq,
|
||||
{
|
||||
self.0.map_mut(|value, changed| {
|
||||
if *value == new_value {
|
||||
*changed = false;
|
||||
} else {
|
||||
*value = new_value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a new reference-based reader for this dynamic value.
|
||||
#[must_use]
|
||||
pub fn create_ref_reader(&self) -> DynamicReader<T> {
|
||||
|
|
@ -207,21 +226,26 @@ impl<T> DynamicData<T> {
|
|||
self.state().wrapped.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R {
|
||||
let mut state = self.state();
|
||||
let old = {
|
||||
let state = &mut *state;
|
||||
let generation = state.wrapped.generation.next();
|
||||
let result = map(&mut state.wrapped.value);
|
||||
state.wrapped.generation = generation;
|
||||
let mut changed = true;
|
||||
let result = map(&mut state.wrapped.value, &mut changed);
|
||||
if changed {
|
||||
state.wrapped.generation = state.wrapped.generation.next();
|
||||
|
||||
for callback in &mut state.callbacks {
|
||||
callback.update(&state.wrapped);
|
||||
}
|
||||
for window in state.windows.drain(..) {
|
||||
window.redraw();
|
||||
for callback in &mut state.callbacks {
|
||||
callback.update(&state.wrapped);
|
||||
}
|
||||
for window in state.windows.drain(..) {
|
||||
window.redraw();
|
||||
}
|
||||
for waker in state.wakers.drain(..) {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
drop(state);
|
||||
|
|
@ -261,6 +285,7 @@ struct State<T> {
|
|||
wrapped: GenerationalValue<T>,
|
||||
callbacks: Vec<Box<dyn ValueCallback<T>>>,
|
||||
windows: Vec<WindowHandle>,
|
||||
wakers: Vec<Waker>,
|
||||
readers: usize,
|
||||
}
|
||||
|
||||
|
|
@ -345,6 +370,14 @@ impl<T> DynamicReader<T> {
|
|||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
}
|
||||
}
|
||||
|
||||
/// Suspends the current async task until the contained value has been
|
||||
/// updated or there are no remaining writers for the value.
|
||||
///
|
||||
/// Returns true if a newly updated value was discovered.
|
||||
pub fn block_until_updated_async(&mut self) -> BlockUntilUpdatedFuture<'_, T> {
|
||||
BlockUntilUpdatedFuture(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for DynamicReader<T> {
|
||||
|
|
@ -364,6 +397,30 @@ impl<T> Drop for DynamicReader<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Suspends the current async task until the contained value has been
|
||||
/// updated or there are no remaining writers for the value.
|
||||
///
|
||||
/// Yeilds true if a newly updated value was discovered.
|
||||
#[derive(Debug)]
|
||||
#[must_use = "futures must be .await'ed to be executed"]
|
||||
pub struct BlockUntilUpdatedFuture<'a, T>(&'a mut DynamicReader<T>);
|
||||
|
||||
impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> {
|
||||
type Output = bool;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
let mut state = self.0.source.state();
|
||||
if state.wrapped.generation != self.0.read_generation {
|
||||
return Poll::Ready(true);
|
||||
} else if state.readers == Arc::strong_count(&self.0.source) {
|
||||
return Poll::Ready(false);
|
||||
}
|
||||
|
||||
state.wakers.push(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnecting_reader_from_dynamic() {
|
||||
let value = Dynamic::new(1);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::fmt::Debug;
|
||||
use std::panic::UnwindSafe;
|
||||
use std::time::Duration;
|
||||
|
||||
use kludgine::app::winit::event::Ime;
|
||||
use kludgine::app::winit::event::{Ime, KeyEvent};
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
|
||||
use kludgine::figures::units::Px;
|
||||
|
|
@ -18,7 +19,7 @@ use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize}
|
|||
use crate::styles::Styles;
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Generation, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
|
||||
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
||||
|
||||
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
|||
pub struct Input {
|
||||
/// The value of this widget.
|
||||
pub text: Value<String>,
|
||||
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
||||
editor: Option<LiveEditor>,
|
||||
cursor_state: CursorState,
|
||||
}
|
||||
|
|
@ -43,9 +45,22 @@ impl Input {
|
|||
text: initial_text.into_value(),
|
||||
editor: None,
|
||||
cursor_state: CursorState::default(),
|
||||
on_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the `on_key` callback.
|
||||
///
|
||||
/// This function is called for every keyboard input event. If [`HANDLED`]
|
||||
/// is returned, this widget will ignore the event.
|
||||
pub fn on_key<F>(mut self, on_key: F) -> Self
|
||||
where
|
||||
F: FnMut(KeyEvent) -> EventHandling + Send + UnwindSafe + 'static,
|
||||
{
|
||||
self.on_key = Some(Callback::new(on_key));
|
||||
self
|
||||
}
|
||||
|
||||
fn editor_mut(&mut self, kludgine: &mut Kludgine, styles: &Styles) -> &mut Editor {
|
||||
match (&self.editor, self.text.generation()) {
|
||||
(Some(editor), generation) if editor.generation == generation => {}
|
||||
|
|
@ -315,6 +330,10 @@ impl Widget for Input {
|
|||
_is_synthetic: bool,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
if let Some(on_key) = &mut self.on_key {
|
||||
on_key.invoke(input.clone())?;
|
||||
}
|
||||
|
||||
if !input.state.is_pressed() {
|
||||
return IGNORED;
|
||||
}
|
||||
|
|
@ -322,11 +341,11 @@ impl Widget for Input {
|
|||
let styles = context.query_styles(&[&TextColor]);
|
||||
let editor = self.editor_mut(context.kludgine, &styles);
|
||||
|
||||
println!(
|
||||
"Keyboard input: {:?}. {:?}, {:?}",
|
||||
input.logical_key, input.text, input.physical_key
|
||||
);
|
||||
let handled = match (input.logical_key, input.text) {
|
||||
// println!(
|
||||
// "Keyboard input: {:?}. {:?}, {:?}",
|
||||
// input.logical_key, input.text, input.physical_key
|
||||
// );
|
||||
let (text_changed, handled) = match (input.logical_key, input.text) {
|
||||
(key @ (Key::Backspace | Key::Delete), _) => {
|
||||
editor.action(
|
||||
context.kludgine.font_system(),
|
||||
|
|
@ -336,7 +355,7 @@ impl Widget for Input {
|
|||
_ => unreachable!("previously matched"),
|
||||
},
|
||||
);
|
||||
HANDLED
|
||||
(true, HANDLED)
|
||||
}
|
||||
(key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => {
|
||||
let modifiers = context.modifiers();
|
||||
|
|
@ -362,18 +381,30 @@ impl Widget for Input {
|
|||
_ => unreachable!("previously matched"),
|
||||
},
|
||||
);
|
||||
HANDLED
|
||||
(false, HANDLED)
|
||||
}
|
||||
(_, Some(text)) if !context.modifiers().state().primary() => {
|
||||
editor.insert_string(&text, None);
|
||||
HANDLED
|
||||
(true, HANDLED)
|
||||
}
|
||||
(_, _) => IGNORED,
|
||||
(_, _) => (false, IGNORED),
|
||||
};
|
||||
|
||||
if handled.is_break() {
|
||||
context.set_needs_redraw();
|
||||
self.cursor_state.force_on();
|
||||
if text_changed {
|
||||
if let Value::Dynamic(value) = &self.text {
|
||||
let state = self.editor.as_mut().expect("just_used");
|
||||
value.map_mut(|text| {
|
||||
text.clear();
|
||||
for line in &state.editor.buffer().lines {
|
||||
text.push_str(line.text());
|
||||
}
|
||||
});
|
||||
state.generation = Some(value.generation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handled
|
||||
|
|
|
|||
|
|
@ -48,27 +48,52 @@ impl ChildWidget {
|
|||
pub struct Scroll {
|
||||
contents: ChildWidget,
|
||||
content_size: Size<Px>,
|
||||
scroll: Point<Px>,
|
||||
max_scroll: Point<Px>,
|
||||
control_size: Size<Px>,
|
||||
scroll: Dynamic<Point<Px>>,
|
||||
enabled: Point<bool>,
|
||||
max_scroll: Dynamic<Point<Px>>,
|
||||
scrollbar_opacity: Dynamic<ZeroToOne>,
|
||||
scrollbar_opacity_animation: AnimationHandle,
|
||||
}
|
||||
|
||||
impl Scroll {
|
||||
/// Returns a new scroll widget containing `contents`.
|
||||
pub fn new(contents: impl MakeWidget) -> Self {
|
||||
fn construct(contents: impl MakeWidget, enabled: Point<bool>) -> Self {
|
||||
Self {
|
||||
contents: ChildWidget::Instance(contents.make_widget()),
|
||||
enabled,
|
||||
content_size: Size::default(),
|
||||
scroll: Point::default(),
|
||||
max_scroll: Point::default(),
|
||||
control_size: Size::default(),
|
||||
scroll: Dynamic::new(Point::default()),
|
||||
max_scroll: Dynamic::new(Point::default()),
|
||||
scrollbar_opacity: Dynamic::default(),
|
||||
scrollbar_opacity_animation: AnimationHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new scroll widget containing `contents` that allows scrolling
|
||||
/// vertically or horizontally.
|
||||
pub fn new(contents: impl MakeWidget) -> Self {
|
||||
Self::construct(contents, Point::new(true, true))
|
||||
}
|
||||
|
||||
/// Returns a new scroll widget that allows scrolling `contents`
|
||||
/// horizontally.
|
||||
pub fn horizontal(contents: impl MakeWidget) -> Self {
|
||||
Self::construct(contents, Point::new(true, false))
|
||||
}
|
||||
|
||||
/// Returns a new scroll widget that allows scrolling `contents` vertically.
|
||||
pub fn vertical(contents: impl MakeWidget) -> Self {
|
||||
Self::construct(contents, Point::new(false, true))
|
||||
}
|
||||
|
||||
fn constrain_scroll(&mut self) {
|
||||
self.scroll = self.scroll.max(self.max_scroll).min(Point::default());
|
||||
let scroll = self.scroll.get();
|
||||
let clamped = scroll.max(self.max_scroll.get()).min(Point::default());
|
||||
if clamped != scroll {
|
||||
self.scroll.set(clamped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,29 +136,68 @@ impl Widget for Scroll {
|
|||
.get_or_default(&ScrollBarThickness)
|
||||
.into_px(context.graphics.scale());
|
||||
|
||||
let mut scroll = self.scroll.get();
|
||||
let current_max_scroll = self.max_scroll.get();
|
||||
|
||||
let control_size = context.graphics.region().size;
|
||||
let max_extents = Size::new(
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.x.into_unsigned()),
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.y.into_unsigned()),
|
||||
if self.enabled.x {
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
|
||||
} else {
|
||||
ConstraintLimit::Known(control_size.width.into_unsigned())
|
||||
},
|
||||
if self.enabled.y {
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
|
||||
} else {
|
||||
ConstraintLimit::Known(control_size.height.into_unsigned())
|
||||
},
|
||||
);
|
||||
let managed = self.contents.managed(&mut context.as_event_context());
|
||||
self.content_size = context
|
||||
let new_content_size = context
|
||||
.for_other(&managed)
|
||||
.measure(max_extents)
|
||||
.into_signed();
|
||||
let control_size = context.graphics.region().size;
|
||||
|
||||
let horizontal_bar = scrollbar_region(scroll.x, new_content_size.width, control_size.width);
|
||||
let max_scroll_x = if self.enabled.x {
|
||||
-horizontal_bar.amount_hidden
|
||||
} else {
|
||||
Px(0)
|
||||
};
|
||||
|
||||
let vertical_bar = scrollbar_region(scroll.y, new_content_size.height, control_size.height);
|
||||
let max_scroll_y = if self.enabled.y {
|
||||
-vertical_bar.amount_hidden
|
||||
} else {
|
||||
Px(0)
|
||||
};
|
||||
|
||||
// Preserve the current scroll if the widget has resized
|
||||
if self.content_size.width != new_content_size.width
|
||||
|| self.control_size.width != control_size.width
|
||||
{
|
||||
self.content_size.width = new_content_size.width;
|
||||
let scroll_pct = scroll.x.into_float() / current_max_scroll.x.into_float();
|
||||
scroll.x = max_scroll_x * scroll_pct;
|
||||
}
|
||||
|
||||
if self.content_size.height != new_content_size.height
|
||||
|| self.control_size.height != control_size.height
|
||||
{
|
||||
self.content_size.height = new_content_size.height;
|
||||
let scroll_pct = scroll.y.into_float() / current_max_scroll.y.into_float();
|
||||
scroll.y = max_scroll_y * scroll_pct;
|
||||
}
|
||||
self.scroll.update(scroll);
|
||||
|
||||
let region = Rect::new(
|
||||
self.scroll,
|
||||
scroll,
|
||||
self.content_size
|
||||
.min(Size::new(Px::MAX, Px::MAX) - self.scroll.max(Point::default())),
|
||||
.min(Size::new(Px::MAX, Px::MAX) - scroll.max(Point::default())),
|
||||
);
|
||||
context.for_child(&managed, region).redraw();
|
||||
|
||||
let horizontal_bar =
|
||||
scrollbar_region(self.scroll.x, self.content_size.width, control_size.width);
|
||||
self.max_scroll.x = -horizontal_bar.amount_hidden;
|
||||
|
||||
if horizontal_bar.size > 0 {
|
||||
if max_scroll_x != 0 {
|
||||
context.graphics.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
|
|
@ -148,11 +212,7 @@ impl Widget for Scroll {
|
|||
);
|
||||
}
|
||||
|
||||
let vertical_bar =
|
||||
scrollbar_region(self.scroll.y, self.content_size.height, control_size.height);
|
||||
self.max_scroll.y = -vertical_bar.amount_hidden;
|
||||
|
||||
if vertical_bar.size > 0 {
|
||||
if max_scroll_y != 0 {
|
||||
context.graphics.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
|
|
@ -166,6 +226,10 @@ impl Widget for Scroll {
|
|||
None,
|
||||
);
|
||||
}
|
||||
|
||||
self.control_size = control_size;
|
||||
self.max_scroll
|
||||
.update(Point::new(max_scroll_x, max_scroll_y));
|
||||
}
|
||||
|
||||
fn measure(
|
||||
|
|
@ -191,7 +255,7 @@ impl Widget for Scroll {
|
|||
}
|
||||
};
|
||||
|
||||
self.scroll += amount.cast();
|
||||
self.scroll.map_mut(|scroll| *scroll += amount.cast());
|
||||
context.set_needs_redraw();
|
||||
|
||||
// TODO make this only returned handled if we actually scrolled.
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ where
|
|||
if !handled {
|
||||
match input.physical_key {
|
||||
KeyCode::KeyW
|
||||
if window.modifiers().state().primary() && dbg!(input.state).is_pressed() =>
|
||||
if window.modifiers().state().primary() && input.state.is_pressed() =>
|
||||
{
|
||||
if self.request_close(&mut window) {
|
||||
window.set_needs_redraw();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use crate::impl_all_tuples;
|
||||
|
||||
/// Invokes a function with a clone of `self`.
|
||||
pub trait WithClone: Sized {
|
||||
/// The type that results from cloning.
|
||||
|
|
|
|||
Loading…
Reference in a new issue