ButtonClick + Overlays at locations

The overlay example now supports right-clicking to open overlays at the
clicked location, showing how a context menu widget can begin being
built.
This commit is contained in:
Jonathan Johnson 2024-05-08 08:38:26 -07:00
parent ecf0d45bfa
commit 1c8d4e0176
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
20 changed files with 165 additions and 76 deletions

View file

@ -68,6 +68,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `PlatformWindowImplementation::set_cursor_icon` and
`PlatformWindow::set_cursor_icon` have been renamed to `set_cursor` and accept
`winit` 0.30's new `Cursor` type.
- `Button::on_click` now takes a `Option<ButtonClick>` structure. When this
value is provided, information about the mouse click that caused the event is
provided.
### Fixed

View file

@ -3,6 +3,7 @@ use std::time::Duration;
use cushy::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn};
use cushy::value::{Destination, Dynamic};
use cushy::widget::MakeWidget;
use cushy::widgets::button::ButtonClick;
use cushy::widgets::progress::Progressable;
use cushy::{Run, WithClone};
use figures::units::Lp;
@ -41,7 +42,7 @@ fn animate_to(
animation: &Dynamic<AnimationHandle>,
value: &Dynamic<u8>,
target: u8,
) -> impl FnMut(()) {
) -> impl FnMut(Option<ButtonClick>) {
(animation, value).with_clone(|(animation, value)| {
move |_| {
// Here we use spawn to schedule the animation, which returns an

View file

@ -27,7 +27,7 @@ fn main() -> cushy::Result {
.into_button()
.on_click({
let task = dynamic.clone();
move |()| {
move |_| {
let background_task = Task::default();
spawn_background_thread(&background_task.progress, &task);
task.set(Some(background_task));

View file

@ -29,7 +29,7 @@ fn main() -> cushy::Result {
.into_button()
.on_click({
let clicked_button = clicked_button.clone();
move |()| clicked_button.set("inner button clicked")
move |_| clicked_button.set("inner button clicked")
})
.with(&ButtonHoverBackground, Color::RED)
.with(&ButtonHoverForeground, Color::WHITE);
@ -40,7 +40,7 @@ fn main() -> cushy::Result {
.into_button()
.on_click({
let clicked_button = clicked_button.clone();
move |()| clicked_button.set("middle button clicked")
move |_| clicked_button.set("middle button clicked")
});
let outer_button = "Yo dawg!"
@ -49,7 +49,7 @@ fn main() -> cushy::Result {
.into_button()
.on_click({
let clicked_button = clicked_button.clone();
move |()| clicked_button.set("outer button clicked")
move |_| clicked_button.set("outer button clicked")
});
outer_button

View file

@ -10,7 +10,7 @@ fn main() -> cushy::Result {
checkbox_state
.clone()
.to_checkbox(label)
.and("Maybe".into_button().on_click(move |()| {
.and("Maybe".into_button().on_click(move |_| {
checkbox_state.set(CheckboxState::Indeterminant);
}))
.into_columns()

View file

@ -87,7 +87,7 @@ fn edit_contact_form(contact: &Contact, db: &Dynamic<HashMap<u64, Contact>>) ->
.on_click({
let contact_id = contact.id;
let db = db.clone();
move |()| {
move |_| {
let mut db = db.lock();
let contact = db.get_mut(&contact_id).expect("missing contact");
contact.first_name = first.get();

View file

@ -19,7 +19,7 @@ fn main() -> cushy::Result {
let info = info.clone();
let window_count = window_count.clone();
let total_windows = total_windows.clone();
move |()| open_a_window(&window_count, &total_windows, &info, &mut app)
move |_| open_a_window(&window_count, &total_windows, &info, &mut app)
})
.make_widget();

View file

@ -64,7 +64,7 @@ fn main() -> cushy::Result {
.and(
"Log In"
.into_button()
.on_click(validations.when_valid(move |()| {
.on_click(validations.when_valid(move |_| {
println!("Welcome, {}", username.get());
exit(0);
}))

View file

@ -51,7 +51,7 @@ fn open_window_button(
let open_windows = open_windows.clone();
let counter = counter.clone();
let texture = texture.clone();
"Open Another Window".into_button().on_click(move |()| {
"Open Another Window".into_button().on_click(move |_| {
open_another_window(&mut app, &open_windows, &counter, &texture);
})
}
@ -82,7 +82,7 @@ fn open_another_window(
.and(
"Close"
.into_button()
.on_click(move |()| handle.request_close()),
.on_click(move |_| handle.request_close()),
)
.into_rows()
.centered(),

View file

@ -4,6 +4,7 @@ use cushy::widgets::layers::{OverlayBuilder, OverlayLayer};
use cushy::Run;
use figures::units::Lp;
use figures::{Point, Zero};
use kludgine::app::winit::event::MouseButton;
use kludgine::Color;
use rand::{thread_rng, Rng};
@ -55,10 +56,20 @@ fn show_overlay_button(
direction_func: impl for<'a> Fn(OverlayBuilder<'a>) -> OverlayBuilder<'a> + Send + 'static,
) -> impl MakeWidget {
let overlay = overlay.clone();
label.into_button().on_click(move |()| {
direction_func(overlay.build_overlay(test_widget(&overlay, false)))
.hide_on_unhover()
.show()
.forget();
})
let button_tag = WidgetTag::unique();
let button_id = button_tag.id();
label
.into_button()
.on_click(move |click| {
let overlay = overlay.build_overlay(test_widget(&overlay, false));
let overlay = match click {
Some(click) if click.mouse_button == MouseButton::Right => {
overlay.above(button_id).at(click.window_location)
}
_ => direction_func(overlay),
};
overlay.hide_on_unhover().show().forget();
})
.make_with_tag(button_tag)
}

View file

@ -80,7 +80,7 @@ fn main() -> cushy::Result {
.and(editors.neutral_variant.1)
.and("Copy to Clipboard".into_button().on_click({
let cushy = app.cushy().clone();
move |()| {
move |_| {
if let Some(mut clipboard) = cushy.clipboard_guard() {
let builder = color_scheme_builder.get();
let mut source = String::default();

View file

@ -22,13 +22,13 @@ fn main() -> cushy::Result {
.and(
"Submit"
.into_button()
.on_click(validations.clone().when_valid(move |()| {
.on_click(validations.clone().when_valid(move |_| {
println!(
"Success! This callback only happens when all associated validations are valid"
);
})),
)
.and("Reset".into_button().on_click(move |()| {
.and("Reset".into_button().on_click(move |_| {
let _value = text.take();
validations.reset();
}))

View file

@ -52,7 +52,7 @@ impl DebugContext {
let id = self.section.map_ref(|section| {
section.values.lock().push(Box::new(RegisteredValue {
label: label.into(),
value: reader.clone(),
_value: reader.clone(),
widget: make_observer(value.weak_clone()).make_widget(),
}))
});
@ -145,13 +145,13 @@ impl Drop for DebugContext {
trait Observable: Send {
fn label(&self) -> &str;
fn alive(&self) -> bool;
// fn alive(&self) -> bool;
fn widget(&self) -> &WidgetInstance;
}
struct RegisteredValue<T> {
label: String,
value: DynamicReader<T>,
_value: DynamicReader<T>,
widget: WidgetInstance,
}
@ -163,9 +163,9 @@ where
&self.label
}
fn alive(&self) -> bool {
self.value.connected()
}
// fn alive(&self) -> bool {
// self.value.connected()
// }
fn widget(&self) -> &WidgetInstance {
&self.widget

View file

@ -30,7 +30,7 @@ pub struct Button {
/// The label to display on the button.
pub content: WidgetRef,
/// The callback that is invoked when the button is clicked.
pub on_click: Option<Callback<()>>,
pub on_click: Option<Callback<Option<ButtonClick>>>,
/// The kind of button to draw.
pub kind: Value<ButtonKind>,
focusable: bool,
@ -158,7 +158,7 @@ impl Button {
#[must_use]
pub fn on_click<F>(mut self, callback: F) -> Self
where
F: FnMut(()) + Send + 'static,
F: FnMut(Option<ButtonClick>) + Send + 'static,
{
self.on_click = Some(Callback::new(callback));
self
@ -171,10 +171,10 @@ impl Button {
self
}
fn invoke_on_click(&mut self, context: &WidgetContext<'_>) {
fn invoke_on_click(&mut self, button: Option<ButtonClick>, context: &WidgetContext<'_>) {
if context.enabled() {
if let Some(on_click) = self.on_click.as_mut() {
on_click.invoke(());
on_click.invoke(button);
}
}
}
@ -461,7 +461,7 @@ impl Widget for Button {
&mut self,
location: Option<Point<Px>>,
_device_id: DeviceId,
_button: MouseButton,
button: MouseButton,
context: &mut EventContext<'_>,
) {
let window_local = self.per_window.entry(context).or_default();
@ -470,12 +470,19 @@ impl Widget for Button {
context.deactivate();
if let (true, Some(location)) = (self.focusable, location) {
if Rect::from(context.last_layout().expect("must have been rendered").size)
.contains(location)
{
let last_layout = context.last_layout().expect("must have been rendered");
// let button_relative
if Rect::from(last_layout.size).contains(location) {
context.focus();
self.invoke_on_click(context);
self.invoke_on_click(
Some(ButtonClick {
mouse_button: button,
location,
window_location: location + last_layout.origin,
}),
context,
);
}
}
}
@ -533,7 +540,7 @@ impl Widget for Button {
// If we have no buttons pressed, the event should fire on activate not
// on deactivate.
if window_local.buttons_pressed == 0 {
self.invoke_on_click(context);
self.invoke_on_click(None, context);
}
self.update_colors(context, true);
}
@ -581,3 +588,14 @@ define_components! {
ButtonDisabledOutline(Color, "disabled_outline_color", Color::CLEAR_BLACK)
}
}
/// A mouse click in a [`Button`].
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct ButtonClick {
/// The mouse button that caused the event.
pub mouse_button: MouseButton,
/// The location relative to the button of the click.
pub location: Point<Px>,
/// The location relative to the window of the click.
pub window_location: Point<Px>,
}

View file

@ -59,7 +59,7 @@ impl MakeWidgetWithTag for Checkbox {
.and(self.label)
.into_columns()
.into_button()
.on_click(move |()| {
.on_click(move |_| {
let mut value = self.state.lock();
*value = !*value;
})

View file

@ -856,6 +856,12 @@ impl<const N: usize> DerefMut for GridWidgets<N> {
#[derive(Debug, Eq, PartialEq)]
pub struct GridSection<const N: usize>([WidgetInstance; N]);
impl Default for GridSection<0> {
fn default() -> Self {
Self::new()
}
}
impl GridSection<0> {
/// Returns an empty section.
#[must_use]

View file

@ -1243,9 +1243,6 @@ where
}
}
#[derive(Debug, PartialEq, Eq)]
struct NotVisible(Point<Px>, usize);
#[derive(Clone, Copy)]
struct BlinkState {
visible: bool,

View file

@ -149,7 +149,7 @@ impl OverlayLayer {
layout: OverlayLayout {
widget: WidgetRef::new(overlay),
relative_to: None,
direction: Direction::Right,
positioning: Position::Relative(Direction::Right),
requires_hover: false,
on_dismiss: None,
layout: None,
@ -395,26 +395,26 @@ impl OverlayState {
context: &mut LayoutContext<'_, '_, '_, '_>,
relative_to: WidgetId,
) -> Option<Rect<Px>> {
let direction = self.overlays[index].direction;
let positioning = self.overlays[index].positioning;
let relative_to = relative_to.find_in(context)?.last_layout()?;
let relative_to_unsigned = relative_to.into_unsigned();
let constraints = match direction {
Direction::Up => Size::new(
let constraints = match positioning {
Position::Relative(Direction::Up) => Size::new(
relative_to_unsigned.size.width,
relative_to_unsigned.origin.y,
),
Direction::Down => Size::new(
Position::Relative(Direction::Down) => Size::new(
relative_to_unsigned.size.width,
available_space.height
- relative_to_unsigned.origin.y
- relative_to_unsigned.size.height,
),
Direction::Left => Size::new(
Position::Relative(Direction::Left) => Size::new(
relative_to_unsigned.origin.x,
relative_to_unsigned.size.height,
),
Direction::Right => Size::new(
Position::Relative(Direction::Right) => Size::new(
available_space.width.saturating_sub(
relative_to_unsigned
.origin
@ -423,6 +423,7 @@ impl OverlayState {
),
relative_to_unsigned.size.height,
),
Position::At(_) => available_space,
};
let size = context
@ -430,26 +431,39 @@ impl OverlayState {
.layout(constraints.map(ConstraintLimit::SizeToFit))
.into_signed();
let mut layout_direction = direction;
let mut layout_direction = positioning;
let mut layout;
loop {
let origin = match layout_direction {
Direction::Up => Point::new(
relative_to.origin.x + relative_to.size.width / 2 - size.width / 2,
relative_to.origin.y - size.height,
let (origin, intersection_matters) = match layout_direction {
Position::Relative(Direction::Up) => (
Point::new(
relative_to.origin.x + relative_to.size.width / 2 - size.width / 2,
relative_to.origin.y - size.height,
),
true,
),
Direction::Down => Point::new(
relative_to.origin.x + relative_to.size.width / 2 - size.width / 2,
relative_to.origin.y + relative_to.size.height,
Position::Relative(Direction::Down) => (
Point::new(
relative_to.origin.x + relative_to.size.width / 2 - size.width / 2,
relative_to.origin.y + relative_to.size.height,
),
true,
),
Direction::Left => Point::new(
relative_to.origin.x - size.width,
relative_to.origin.y + relative_to.size.height / 2 - size.height / 2,
Position::Relative(Direction::Left) => (
Point::new(
relative_to.origin.x - size.width,
relative_to.origin.y + relative_to.size.height / 2 - size.height / 2,
),
true,
),
Direction::Right => Point::new(
relative_to.origin.x + relative_to.size.width,
relative_to.origin.y + relative_to.size.height / 2 - size.height / 2,
Position::Relative(Direction::Right) => (
Point::new(
relative_to.origin.x + relative_to.size.width,
relative_to.origin.y + relative_to.size.height / 2 - size.height / 2,
),
true,
),
Position::At(pt) => (pt, false),
};
layout = Rect::new(origin.max(Point::ZERO), size);
@ -462,16 +476,27 @@ impl OverlayState {
layout.origin.y -= bottom_right.y - available_space.height.into_signed();
}
if layout.intersects(&relative_to) || self.layout_intersects(index, &layout, context) {
layout_direction = layout_direction.next_clockwise();
if layout_direction == direction {
// No layout worked optimally.
if intersection_matters
&& (layout.intersects(&relative_to)
|| self.layout_intersects(index, &layout, context))
{
if let Some(next_direction) = layout_direction.next_clockwise() {
if layout_direction == positioning {
// No layout worked optimally.
break;
}
layout_direction = next_direction;
} else {
break;
}
} else {
break;
}
}
// TODO check to ensure the widget is fully on-window, otherwise attempt
// to shift it to become visible.
Some(layout)
}
@ -516,7 +541,7 @@ impl OverlayState {
if let Some(relative_to) = self.overlays[index].relative_to {
self.layout_overlay_relative(index, widget, available_space, context, relative_to)
} else {
let direction = self.overlays[index].direction;
let direction = self.overlays[index].positioning;
let size = context
.for_other(widget)
.layout(available_space.map(ConstraintLimit::SizeToFit))
@ -525,22 +550,23 @@ impl OverlayState {
let available_space = available_space.into_signed();
let origin = match direction {
Direction::Up => Point::new(
Position::Relative(Direction::Up) => Point::new(
available_space.width / 2,
(available_space.height - size.height) / 2,
),
Direction::Down => Point::new(
Position::Relative(Direction::Down) => Point::new(
available_space.width / 2,
available_space.height / 2 + size.height / 2,
),
Direction::Right => Point::new(
Position::Relative(Direction::Right) => Point::new(
available_space.width / 2 + size.width / 2,
available_space.height / 2,
),
Direction::Left => Point::new(
Position::Relative(Direction::Left) => Point::new(
(available_space.width - size.width) / 2,
available_space.height / 2,
),
Position::At(pt) => pt,
};
Some(Rect::new(origin, size))
@ -592,7 +618,14 @@ impl OverlayBuilder<'_> {
#[must_use]
pub fn near(mut self, id: WidgetId, direction: Direction) -> Self {
self.layout.relative_to = Some(id);
self.layout.direction = direction;
self.layout.positioning = Position::Relative(direction);
self
}
/// Shows this overlay at a specified window `location`.
#[must_use]
pub fn at(mut self, location: Point<Px>) -> Self {
self.layout.positioning = Position::At(location);
self
}
@ -632,7 +665,7 @@ struct OverlayLayout {
widget: WidgetRef,
opacity: Dynamic<ZeroToOne>,
relative_to: Option<WidgetId>,
direction: Direction,
positioning: Position<Px>,
requires_hover: bool,
layout: Option<Rect<Px>>,
on_dismiss: Option<Arc<Mutex<Callback>>>,
@ -654,7 +687,7 @@ impl PartialEq for OverlayLayout {
self.widget == other.widget
&& self.opacity == other.opacity
&& self.relative_to == other.relative_to
&& self.direction == other.direction
&& self.positioning == other.positioning
&& self.requires_hover == other.requires_hover
&& self.layout == other.layout
&& match (&self.on_dismiss, &other.on_dismiss) {
@ -665,6 +698,26 @@ impl PartialEq for OverlayLayout {
}
}
/// An overlay position.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Position<T> {
/// Relative to the parent in a given direction.
Relative(Direction),
/// At a window coordinate.
At(Point<T>),
}
impl<T> Position<T> {
/// Returns the next direction when rotating clockwise.
#[must_use]
pub fn next_clockwise(&self) -> Option<Self> {
match self {
Self::Relative(direction) => Some(Self::Relative(direction.next_clockwise())),
Self::At(_) => None,
}
}
}
/// A relative direction.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction {
@ -674,7 +727,7 @@ pub enum Direction {
Right,
/// Positive along the Y axis.
Down,
/// Legative along the X axis.
/// Negative along the X axis.
Left,
}

View file

@ -62,7 +62,7 @@ where
.and(self.label)
.into_columns()
.into_button()
.on_click(move |()| {
.on_click(move |_| {
self.state.set(self.value.clone());
})
.kind(self.kind)

View file

@ -73,7 +73,7 @@ where
});
self.label
.into_button()
.on_click(move |()| {
.on_click(move |_| {
self.state.set(self.value.clone());
})
.kind(kind)