From 4f3ef7d9edfa7c614574d9b1b7d819cd0a292b02 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Fri, 4 Oct 2024 10:17:40 -0700 Subject: [PATCH] Nested modals --- CHANGELOG.md | 6 + Cargo.lock | 16 +-- Cargo.toml | 2 +- examples/nested-modals.rs | 50 ++++++++ src/context.rs | 7 +- src/dialog.rs | 19 ++- src/value.rs | 15 ++- src/widgets/layers.rs | 264 +++++++++++++++++++++++++++++--------- src/widgets/list.rs | 8 +- src/widgets/space.rs | 20 ++- src/window.rs | 2 + 11 files changed, 325 insertions(+), 84 deletions(-) create mode 100644 examples/nested-modals.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e98da800f..74f292d10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 winit, and naga. Thanks to @bluenote10 for the feedback! - `WrapperWidget::activate`'s default implementation now activates the wrapped widget. +- `Space` now intercepts mouse events if its color has a non-zero alpha channel. ### Fixed @@ -91,6 +92,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Dynamic` change callbacks has been fixed. - `Stack` no longer unwraps a `Resize` child if the resize widget is resizing in the direction opposite of the Stack's orientation. +- If the layout of widgets changes during a redraw, the currently hovered widget + is now properly updated immediately. Previously, the hover would only update + on the next cursor event. ### Added @@ -211,6 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Choosing one or more files - Choosing a single folder/directory - Choosing one or more folders/directories +- `DynamicGuard::unlocked` executes a closure while the guard is temporarily + unlocked. [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/Cargo.lock b/Cargo.lock index cc87edd08..4ce290d48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -75,9 +75,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alot" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b072fc284b73a3e4154e2decdbaad711daca0e8fedfceb0d7b1cbe2dffb00e2b" +checksum = "4c7a3dc3ad32931b2d6e97c99a702208dfd1e2c446580e5f99d1d8355df26db6" [[package]] name = "android-activity" @@ -1382,9 +1382,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gl_generator" @@ -2403,9 +2403,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index eeca8714a..9e901da9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [ "app", ] } figures = { version = "0.4.0" } -alot = "0.3" +alot = "0.3.2" interner = "0.2.1" kempt = "0.2.1" intentional = "0.1.0" diff --git a/examples/nested-modals.rs b/examples/nested-modals.rs new file mode 100644 index 000000000..dc488936f --- /dev/null +++ b/examples/nested-modals.rs @@ -0,0 +1,50 @@ +use cushy::dialog::MessageBox; +use cushy::widget::MakeWidget; +use cushy::widgets::layers::{Modal, ModalTarget}; +use cushy::Run; + +fn main() -> cushy::Result { + let modal = Modal::new(); + + "Show Modal" + .into_button() + .on_click({ + let modal = modal.clone(); + move |_| show_modal(&modal, 1) + }) + .align_top() + .pad() + .and(modal) + .into_layers() + .run() +} + +fn show_modal(present_in: &impl ModalTarget, level: usize) { + let handle = present_in.pending_handle(); + handle + .build_dialog( + format!("Modal level: {level}") + .and("Go Deeper".into_button().on_click({ + let handle = handle.clone(); + move |_| { + show_modal(&handle, level + 1); + } + })) + .and("Show message".into_button().on_click({ + let handle = handle.clone(); + move |_| { + MessageBox::message("This is a MessageBox shown above a modal") + .open(&handle); + } + })) + .into_rows(), + ) + .with_default_button("Close", || {}) + .with_cancel_button("Close All", { + let handle = handle.clone(); + move || { + handle.layer().dismiss(); + } + }) + .show(); +} diff --git a/src/context.rs b/src/context.rs index d597940b9..2334fe10c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -307,7 +307,8 @@ impl<'context> EventContext<'context> { } pub(crate) fn update_hovered_widget(&mut self) { - self.cursor.widget = None; + let current_hover = self.cursor.widget.take(); + if let Some(location) = self.cursor.location { for widget in self.tree.widgets_under_point(location) { let mut widget_context = self.for_other(&widget); @@ -317,7 +318,9 @@ impl<'context> EventContext<'context> { let relative = location - widget_layout.origin; if widget_context.hit_test(relative) { - widget_context.hover(location); + if current_hover != Some(widget.id()) { + widget_context.hover(location); + } drop(widget_context); self.cursor.widget = Some(widget.id()); break; diff --git a/src/dialog.rs b/src/dialog.rs index 6f77626a7..46313a225 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -15,7 +15,7 @@ use crate::value::{Destination, Dynamic, Source}; use crate::widget::{MakeWidget, OnceCallback, SharedCallback, WidgetList}; use crate::widgets::button::{ButtonKind, ClickCounter}; use crate::widgets::input::InputValue; -use crate::widgets::layers::Modal; +use crate::widgets::layers::{Modal, ModalTarget}; use crate::widgets::Custom; use crate::ModifiersExt; @@ -274,7 +274,10 @@ impl MessageBox { /// Opens this dialog in the given target. /// - /// A target can be a [`Modal`] layer, a [`WindowHandle`], or an [`App`]. + /// A target can be a [`Modal`] layer, a + /// [`ModalHandle`](crate::widgets::layers::ModalHandle), a + /// [`WindowHandle`](crate::window::WindowHandle), or an + /// [`App`](crate::App). pub fn open(&self, open_in: &impl OpenMessageBox) { open_in.open_message_box(self); } @@ -294,9 +297,13 @@ fn coalesce_empty<'a>(s1: &'a str, s2: &'a str) -> &'a str { } } -impl OpenMessageBox for Modal { +impl OpenMessageBox for T +where + T: ModalTarget, +{ fn open_message_box(&self, message: &MessageBox) { - let dialog = self.build_dialog( + let handle = self.pending_handle(); + let dialog = handle.build_dialog( message .title .as_str() @@ -716,7 +723,9 @@ impl MakeWidget for FilePickerWidget { }; let chosen_paths = Dynamic::>::default(); - let confirm_enabled = chosen_paths.map_each(|paths| !paths.is_empty()); + let confirm_enabled = chosen_paths.map_each(move |paths| { + !paths.is_empty() && paths.iter().all(|p| p.is_file() == kind.is_file()) + }); let browsing_directory = Dynamic::new( self.picker diff --git a/src/value.rs b/src/value.rs index 1fd0011b7..fcdbb86f8 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1536,11 +1536,12 @@ where } impl<'a, T> DynamicMutexGuard<'a, T> { - fn unlocked(&mut self, while_unlocked: impl FnOnce()) { + fn unlocked(&mut self, while_unlocked: impl FnOnce() -> R) -> R { let previous_state = self.dynamic.during_callback_state.lock().take(); - MutexGuard::unlocked(&mut self.guard, while_unlocked); + let result = MutexGuard::unlocked(&mut self.guard, while_unlocked); *self.dynamic.during_callback_state.lock() = previous_state; + result } } @@ -2196,7 +2197,7 @@ impl<'a, T> DynamicOrOwnedGuard<'a, T> { } } - fn unlocked(&mut self, while_unlocked: impl FnOnce()) { + fn unlocked(&mut self, while_unlocked: impl FnOnce() -> R) -> R { match self { Self::Dynamic(guard) => guard.unlocked(while_unlocked), Self::Owned(_) | Self::OwnedRef(_) => while_unlocked(), @@ -2252,6 +2253,14 @@ impl DynamicGuard<'_, T, READONLY> { pub fn prevent_notifications(&mut self) { self.prevent_notifications = true; } + + /// Executes `while_unlocked` while this guard is temporarily unlocked. + pub fn unlocked(&mut self, while_unlocked: F) -> R + where + F: FnOnce() -> R, + { + self.guard.unlocked(while_unlocked) + } } impl<'a, T, const READONLY: bool> Deref for DynamicGuard<'a, T, READONLY> { diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 1dcb207f5..71be838fe 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -8,17 +8,18 @@ use alot::{LotId, OrderedLots}; use cushy::widget::{RootBehavior, WidgetInstance}; use easing_function::EasingFunction; use figures::units::{Lp, Px, UPx}; -use figures::{IntoComponents, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero}; +use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero}; use intentional::Assert; use super::super::widget::MountedWidget; +use super::Space; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; -use crate::styles::components::{EasingIn, IntrinsicPadding, ScrimColor}; -use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value}; +use crate::styles::components::{EasingIn, ScrimColor}; +use crate::value::{Destination, Dynamic, DynamicGuard, DynamicRead, IntoValue, Source, Value}; use crate::widget::{ - Callback, MakeWidget, MountedChildren, SharedCallback, Widget, WidgetId, WidgetList, WidgetRef, - WrapperWidget, + Callback, MakeWidget, MakeWidgetWithTag, MountedChildren, SharedCallback, Widget, WidgetId, + WidgetList, WidgetRef, WidgetTag, WrapperWidget, }; use crate::widgets::container::ContainerShadow; use crate::ConstraintLimit; @@ -892,7 +893,7 @@ impl WrapperWidget for Tooltipped { /// Designed to be used in a [`Layers`] widget. #[derive(Debug, Clone, Default)] pub struct Modal { - modal: Dynamic>, + modal: Dynamic>, } impl Modal { @@ -906,7 +907,23 @@ impl Modal { /// Presents `contents` as the modal session. pub fn present(&self, contents: impl MakeWidget) { - self.modal.set(Some(contents.make_widget())); + self.present_inner(contents); + } + + fn present_inner(&self, contents: impl MakeWidget) -> LotId { + let mut state = self.modal.lock(); + state.push(contents.make_widget()) + } + + /// Returns a new pending handle that can be used to show a modal and + /// dismiss it. + #[must_use] + pub fn pending_handle(&self) -> ModalHandle { + ModalHandle { + layer: self.clone(), + above: None, + id: Dynamic::default(), + } } /// Presents a modal dialog containing `message` with a default button that @@ -919,18 +936,18 @@ impl Modal { /// Returns a builder for a modal dialog that displays `message`. pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder { - DialogBuilder::new(self, message) + DialogBuilder::new(self.pending_handle(), message) } /// Dismisses the modal session. pub fn dismiss(&self) { - self.modal.set(None); + self.modal.lock().clear(); } /// Returns true if this layer is currently presenting a modal session. #[must_use] pub fn visible(&self) -> bool { - self.modal.map_ref(Option::is_some) + !self.modal.lock().is_empty() } /// Returns a function that dismisses the modal when invoked. @@ -946,67 +963,97 @@ impl Modal { } } -impl MakeWidget for Modal { - fn make_widget(self) -> WidgetInstance { +impl MakeWidgetWithTag for Modal { + fn make_with_tag(self, tag: WidgetTag) -> WidgetInstance { + let layer_widgets = Dynamic::default(); + ModalLayer { - presented: None, + layers: WidgetRef::new(Layers::new(layer_widgets.clone())), + layer_widgets, + presented: Vec::new(), + focus_top_layer: false, modal: self.modal, } - .make_widget() + .make_with_tag(tag) } } #[derive(Debug)] struct ModalLayer { - presented: Option, - modal: Dynamic>, + presented: Vec, + layer_widgets: Dynamic, + layers: WidgetRef, + modal: Dynamic>, + focus_top_layer: bool, } -impl Widget for ModalLayer { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { - if let Some(presented) = &self.presented { - let bg = context.get(&ScrimColor); - context.fill(bg); - context.for_other(presented).redraw(); - } +impl WrapperWidget for ModalLayer { + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.layers } - fn layout( + fn adjust_child_constraints( &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { - let modal = self.modal.get_tracking_invalidate(context); - if self.presented.as_ref().map(MountedWidget::instance) != modal.as_ref() { - if let Some(presented) = self.presented.take() { - context.remove_child(&presented); + ) -> Size { + self.modal.invalidate_when_changed(context); + let modal = self.modal.read(); + let mut layer_widgets = self.layer_widgets.lock(); + self.focus_top_layer = false; + for index in 0..modal.len().min(self.presented.len()) { + let modal_widget = &modal[index]; + let presented = &mut self.presented[index]; + if presented != modal_widget { + let modal_widget = modal_widget.clone(); + *presented = modal_widget.clone(); + layer_widgets[index * 2 + 1] = modal_widget.clone().centered().make_widget(); + + self.focus_top_layer = true; } - self.presented = modal.map(|modal| { - let mounted = context.push_child(modal); - context.for_other(&mounted).focus(); - mounted - }); } - let full_area = available_space.map(ConstraintLimit::max); - if let Some(child) = &self.presented { - let padding = context.get(&IntrinsicPadding); - let layout_size = full_area - Size::squared(padding.into_upx(context.gfx.scale())); - let child_size = context - .for_other(child) - .layout(layout_size.map(ConstraintLimit::SizeToFit)) - .into_signed(); - let margin = full_area.into_signed() - child_size; - context.set_child_layout( - child, - Rect::new(margin.to_vec::>() / 2, child_size), - ); + + for to_present in modal.iter().skip(self.presented.len()) { + self.focus_top_layer = true; + layer_widgets.push(Space::colored(context.get(&ScrimColor))); + self.presented.push(to_present.clone()); + layer_widgets.push(to_present.clone().centered()); + } + + if self.presented.len() > modal.len() { + self.presented.truncate(modal.len()); + layer_widgets.truncate(modal.len() * 2); + self.focus_top_layer = true; } - full_area + available_space } - fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { - self.presented.is_some() + fn position_child( + &mut self, + size: Size, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_>, + ) -> crate::widget::WrappedLayout { + if self.focus_top_layer { + self.focus_top_layer = false; + if let Some(mut ctx) = self + .presented + .last() + .and_then(|topmost| context.for_other(topmost)) + { + ctx.focus(); + } + } + Size::new( + available_space + .width + .fit_measured(size.width, context.gfx.scale()), + available_space + .height + .fit_measured(size.height, context.gfx.scale()), + ) + .into() } } @@ -1020,16 +1067,16 @@ pub enum Yes {} /// A modal dialog builder. #[must_use = "DialogBuilder::show must be called for the dialog to be shown"] pub struct DialogBuilder { - modal: Modal, + handle: ModalHandle, message: WidgetInstance, buttons: WidgetList, _state: PhantomData<(HasDefault, HasCancel)>, } impl DialogBuilder { - fn new(modal: &Modal, message: impl MakeWidget) -> Self { + fn new(handle: ModalHandle, message: impl MakeWidget) -> Self { Self { - modal: modal.clone(), + handle, message: message.make_widget(), buttons: WidgetList::new(), _state: PhantomData, @@ -1065,7 +1112,7 @@ impl DialogBuilder { on_click: impl FnOnce() + Send + 'static, ) { let mut on_click = Some(on_click); - let modal = self.modal.clone(); + let modal = self.handle.clone(); let mut button = caption .into_button() .on_click(move |_| { @@ -1084,12 +1131,12 @@ impl DialogBuilder { self.buttons.push(button.fit_horizontally().make_widget()); } - /// Shows the modal dialog. + /// Shows the modal dialog, returning a handle that owns the session. pub fn show(mut self) { if self.buttons.is_empty() { self.inner_push_button("OK", DialogButtonKind::Default, || {}); } - self.modal.present( + self.handle.present( self.message .and(self.buttons.into_columns().centered()) .into_rows() @@ -1108,13 +1155,13 @@ impl DialogBuilder { ) -> DialogBuilder { self.inner_push_button(caption, DialogButtonKind::Default, on_click); let Self { - modal, + handle, message, buttons, _state, } = self; DialogBuilder { - modal, + handle, message, buttons, _state: PhantomData, @@ -1132,13 +1179,13 @@ impl DialogBuilder { ) -> DialogBuilder { self.inner_push_button(caption, DialogButtonKind::Cancel, on_click); let Self { - modal, + handle, message, buttons, _state, } = self; DialogBuilder { - modal, + handle, message, buttons, _state: PhantomData, @@ -1152,3 +1199,100 @@ enum DialogButtonKind { Default, Cancel, } + +/// A handle to a modal dialog presented in a [`Modal`] layer. +#[derive(Clone)] +pub struct ModalHandle { + layer: Modal, + above: Option>>, + id: Dynamic>, +} + +impl ModalHandle { + fn above(mut self, other: &Self) -> Self { + self.above = Some(other.id.clone()); + self + } + + /// Presents `contents` as a modal dialog, updating this handle to control + /// it. + pub fn present(&self, contents: impl MakeWidget) { + let mut state = self.layer.modal.lock(); + if let Some(above) = self.above.as_ref().and_then(Source::get) { + if let Some(index) = state.index_of_id(above) { + state.truncate(index + 1); + } else { + self.id.set(None); + return; + } + } else { + state.clear(); + }; + self.id.set(Some(state.push(contents.make_widget()))); + } + + // /// Prevents the modal shown by this handle from being dismissed when the + // /// last reference is dropped. + // pub fn persist(self) { + // self.id.set(None); + // drop(self); + // } + + /// Dismisses the modal shown by this handle. + pub fn dismiss(&self) { + let Some(id) = self.id.take() else { return }; + let mut state = self.layer.modal.lock(); + let Some(index) = state.index_of_id(id) else { + return; + }; + state.truncate(index); + } + + /// Returns the modal layer the dialog is presented on. + #[must_use] + pub const fn layer(&self) -> &Modal { + &self.layer + } + + /// Returns a builder for a modal dialog that displays `message` in a modal + /// dialog above the dialog shown by this handle. + pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder { + DialogBuilder::new(self.clone(), message) + } +} + +impl Drop for ModalHandle { + fn drop(&mut self) { + if self.id.instances() == 1 { + self.dismiss(); + } + } +} + +/// A target for a [`Modal`] session. +pub trait ModalTarget: Send + 'static { + /// Returns a new handle that can be used to show a dialog above `self`. + fn pending_handle(&self) -> ModalHandle; + /// Returns a reference to the modal layer this target presents to. + fn layer(&self) -> &Modal; +} + +impl ModalTarget for Modal { + fn pending_handle(&self) -> ModalHandle { + self.pending_handle() + } + + fn layer(&self) -> &Modal { + self + } +} + +impl ModalTarget for ModalHandle { + fn pending_handle(&self) -> ModalHandle { + self.layer.pending_handle().above(self) + } + + fn layer(&self) -> &Modal { + &self.layer + } +} diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 790c63059..db741588b 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -20,7 +20,7 @@ use super::label::DynamicDisplay; use super::{Grid, Label}; use crate::styles::{Component, RequireInvalidation}; use crate::value::{IntoValue, MapEach, Source, Value}; -use crate::widget::{MakeWidget, WidgetInstance, WidgetList}; +use crate::widget::{MakeWidget, MakeWidgetWithTag, WidgetInstance, WidgetList}; /// A list of items displayed with an optional item indicator. pub struct List { @@ -445,8 +445,8 @@ impl ListIndicator for ListStyle { } } -impl MakeWidget for List { - fn make_widget(self) -> WidgetInstance { +impl MakeWidgetWithTag for List { + fn make_with_tag(self, tag: crate::widget::WidgetTag) -> WidgetInstance { let rows = match (self.children, self.style) { (children, Value::Constant(style)) => { children.map_each(move |children| build_grid_widgets(&style, children)) @@ -459,7 +459,7 @@ impl MakeWidget for List { Value::Dynamic(style.map_each(move |style| build_grid_widgets(style, &children))) } }; - Grid::from_rows(rows).make_widget() + Grid::from_rows(rows).make_with_tag(tag) } } diff --git a/src/widgets/space.rs b/src/widgets/space.rs index 64c89772f..ffdfa0258 100644 --- a/src/widgets/space.rs +++ b/src/widgets/space.rs @@ -4,7 +4,7 @@ use kludgine::Color; use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::PrimaryColor; -use crate::styles::{DynamicComponent, IntoDynamicComponentValue}; +use crate::styles::{Component, DynamicComponent, IntoDynamicComponentValue}; use crate::value::{IntoValue, Value}; use crate::widget::Widget; use crate::ConstraintLimit; @@ -76,6 +76,24 @@ impl Widget for Space { ) -> Size { available_space.map(ConstraintLimit::min) } + + fn hit_test( + &mut self, + _location: figures::Point, + context: &mut crate::context::EventContext<'_>, + ) -> bool { + let color = match self.color.get() { + ColorSource::Color(color) => color, + ColorSource::Dynamic(dynamic_component) => { + if let Some(Component::Color(color)) = dynamic_component.resolve(context) { + color + } else { + return false; + } + } + }; + color.alpha() > 0 + } } #[derive(Debug, PartialEq, Clone)] diff --git a/src/window.rs b/src/window.rs index e6ac469fc..8c044486b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1960,6 +1960,8 @@ where self.outer_size.set(layout_context.window().outer_size()); self.root.invalidate(); } + + layout_context.as_event_context().update_hovered_widget(); } fn close_requested(&mut self, window: W, kludgine: &mut Kludgine) -> bool