diff --git a/examples/hello.rs b/examples/hello.rs index 464501d1c..1de432ab6 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,35 +1,43 @@ -use xilem::view::{button, h_stack, v_stack}; +use xilem::view::{button, h_stack, switch, v_stack}; use xilem::{view::View, App, AppLauncher}; -fn app_logic(data: &mut i32) -> impl View { +fn app_logic(data: &mut AppData) -> impl View { // here's some logic, deriving state for the view from our state - let label = if *data == 1 { + let count = data.count; + let label = if count == 1 { "clicked 1 time".to_string() } else { - format!("clicked {data} times") + format!("clicked {count} times") }; // The actual UI Code starts here v_stack(( - button(label, |data| { + button(label, |data: &mut AppData| { println!("clicked"); - *data += 1; + data.count += 1; }), h_stack(( - "Buttons: ", - button("decrease", |data| { + button("decrease", |data: &mut AppData| { println!("clicked decrease"); - *data -= 1; + data.count -= 1; }), - button("reset", |data| { + button("reset", |data: &mut AppData| { println!("clicked reset"); - *data = 0; + data.count = 0; + }), + switch(data.is_on, |data: &mut AppData, value: bool| { + data.is_on = value }), )), )) .with_spacing(20.0) } +struct AppData { + count: i32, + is_on: bool, +} + fn main() { /* let app = Application::new().unwrap(); @@ -41,6 +49,11 @@ fn main() { window_handle.show(); app.run(None); */ - let app = App::new(0, app_logic); + let data = AppData { + count: 0, + is_on: false, + }; + + let app = App::new(data, app_logic); AppLauncher::new(app).run() } diff --git a/src/view/mod.rs b/src/view/mod.rs index 86c066d3a..9ad8c7270 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -21,6 +21,7 @@ mod text; // mod use_state; mod linear_layout; mod list; +mod switch; #[allow(clippy::module_inception)] mod view; @@ -29,4 +30,5 @@ pub use xilem_core::{Id, IdPath, VecSplice}; pub use button::button; pub use linear_layout::{h_stack, v_stack, LinearLayout}; pub use list::{list, List}; +pub use switch::switch; pub use view::{Adapt, AdaptState, Cx, Memoize, View, ViewMarker, ViewSequence}; diff --git a/src/view/switch.rs b/src/view/switch.rs new file mode 100644 index 000000000..009490383 --- /dev/null +++ b/src/view/switch.rs @@ -0,0 +1,81 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use crate::view::ViewMarker; +use crate::{view::Id, widget::ChangeFlags, MessageResult}; + +use super::{Cx, View}; + +pub struct Switch { + is_on: bool, + #[allow(clippy::type_complexity)] + callback: Box A + Send>, +} + +pub fn switch( + is_on: bool, + clicked: impl Fn(&mut T, bool) -> A + Send + 'static, +) -> Switch { + Switch::new(is_on, clicked) +} + +impl Switch { + pub fn new(is_on: bool, clicked: impl Fn(&mut T, bool) -> A + Send + 'static) -> Self { + Switch { + is_on, + callback: Box::new(clicked), + } + } +} + +impl ViewMarker for Switch {} + +impl View for Switch { + type State = (); + + type Element = crate::widget::Switch; + + fn build(&self, cx: &mut Cx) -> (crate::view::Id, Self::State, Self::Element) { + let (id, element) = + cx.with_new_id(|cx| crate::widget::Switch::new(cx.id_path(), self.is_on)); + (id, (), element) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + if prev.is_on != self.is_on { + element.set_is_on(self.is_on) + } else { + ChangeFlags::default() + } + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + _message: Box, + app_state: &mut T, + ) -> MessageResult { + MessageResult::Action((self.callback)(app_state, !self.is_on)) + } +} diff --git a/src/widget/button.rs b/src/widget/button.rs index 48c5884d0..2c8708d9c 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -108,6 +108,7 @@ impl Widget for Button { ); self.layout = Some(layout); //(Size::new(10.0, min_height), size) + cx.request_paint(); bc.constrain(size) } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 102154567..1e629063f 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -21,6 +21,7 @@ mod core; mod linear_layout; mod piet_scene_helpers; mod raw_event; +mod switch; //mod scroll_view; mod text; #[allow(clippy::module_inception)] @@ -33,5 +34,6 @@ pub use button::Button; pub use contexts::{AccessCx, CxState, EventCx, LayoutCx, LifeCycleCx, PaintCx, UpdateCx}; pub use linear_layout::LinearLayout; pub use raw_event::{Event, LifeCycle, MouseEvent, ViewContext}; +pub use switch::Switch; pub use text::TextWidget; pub use widget::{AnyWidget, Widget}; diff --git a/src/widget/piet_scene_helpers.rs b/src/widget/piet_scene_helpers.rs index 64884e270..9e80a7aac 100644 --- a/src/widget/piet_scene_helpers.rs +++ b/src/widget/piet_scene_helpers.rs @@ -1,5 +1,5 @@ use vello::kurbo::{self, Affine, Rect, Shape}; -use vello::peniko::{BrushRef, ColorStopsSource, Fill, Gradient, Stroke}; +use vello::peniko::{BrushRef, Color, ColorStopsSource, Fill, Gradient, Stroke}; use vello::SceneBuilder; #[derive(Debug, Clone, Copy)] @@ -73,3 +73,7 @@ pub fn fill_lin_gradient( let brush = Gradient::new_linear(start.resolve(rect), end.resolve(rect)).with_stops(stops); builder.fill(Fill::NonZero, Affine::IDENTITY, &brush, None, path); } + +pub fn fill_color(builder: &mut SceneBuilder, path: &impl Shape, color: Color) { + builder.fill(Fill::NonZero, Affine::IDENTITY, color, None, path) +} diff --git a/src/widget/switch.rs b/src/widget/switch.rs new file mode 100644 index 000000000..4ca34a6d0 --- /dev/null +++ b/src/widget/switch.rs @@ -0,0 +1,165 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use glazier::kurbo::Circle; +use vello::{ + kurbo::{Point, Size}, + peniko::Color, + SceneBuilder, +}; + +use crate::{IdPath, Message}; + +use super::{ + contexts::LifeCycleCx, + piet_scene_helpers::{fill_color, stroke}, + AccessCx, BoxConstraints, ChangeFlags, Event, EventCx, LayoutCx, LifeCycle, PaintCx, UpdateCx, + Widget, +}; + +pub struct Switch { + id_path: IdPath, + is_on: bool, + is_dragging: bool, + knob_position: Point, +} + +impl Switch { + pub fn new(id_path: &IdPath, is_on: bool) -> Switch { + Switch { + id_path: id_path.clone(), + is_on, + is_dragging: false, + knob_position: if is_on { + Point::new(ON_POS, KNOB_DIAMETER / 2. + SWITCH_PADDING) + } else { + Point::new(OFF_POS, KNOB_DIAMETER / 2. + SWITCH_PADDING) + }, + } + } + + pub fn set_is_on(&mut self, is_on: bool) -> ChangeFlags { + self.is_on = is_on; + ChangeFlags::PAINT + } +} + +// See druid's button for info. +const KNOB_DIAMETER: f64 = 20.0; +const SWITCH_PADDING: f64 = 3.0; +const SWITCH_WIDTH: f64 = 2.0 * KNOB_DIAMETER + 2.0 * SWITCH_PADDING; +const SWITCH_HEIGHT: f64 = KNOB_DIAMETER + 2.0 * SWITCH_PADDING; +const ON_POS: f64 = SWITCH_WIDTH - KNOB_DIAMETER / 2.0 - SWITCH_PADDING; +const OFF_POS: f64 = KNOB_DIAMETER / 2.0 + SWITCH_PADDING; + +impl Widget for Switch { + fn event(&mut self, cx: &mut EventCx, event: &Event) { + match event { + Event::MouseDown(_) => { + cx.set_active(true); + cx.request_paint(); + } + Event::MouseUp(_) => { + if self.is_dragging { + if self.is_on != (self.knob_position.x > SWITCH_WIDTH / 2.0) { + cx.add_message(Message::new(self.id_path.clone(), ())) + } + } else if cx.is_active() { + cx.add_message(Message::new(self.id_path.clone(), ())); + } + // Reset Flags + cx.set_active(false); + self.is_dragging = false; + + // Request repaint + cx.request_paint(); + } + Event::MouseMove(mouse) => { + if cx.is_active() { + self.knob_position.x = mouse.pos.x.clamp(OFF_POS, ON_POS); + self.is_dragging = true; + println!("Mouse Move{:?}", self.knob_position); + } + cx.request_paint(); + } + Event::TargetedAccessibilityAction(request) => { + if request.action == accesskit::Action::Default + && cx.is_accesskit_target(request.target) + { + cx.add_message(Message::new(self.id_path.clone(), ())); + } + } + _ => (), + }; + } + + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + if let LifeCycle::HotChanged(_) = event { + cx.request_paint(); + } + } + + fn update(&mut self, cx: &mut UpdateCx) { + cx.request_layout(); + } + + fn layout(&mut self, _cx: &mut LayoutCx, _bc: &BoxConstraints) -> Size { + Size::new(SWITCH_WIDTH, SWITCH_HEIGHT) + } + + fn accessibility(&mut self, cx: &mut AccessCx) { + let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Switch); + builder.set_default_action_verb(accesskit::DefaultActionVerb::Click); + cx.push_node(builder); + } + + fn paint(&mut self, cx: &mut PaintCx, builder: &mut SceneBuilder) { + // Change the position of of the knob based on its state + // If the knob is currently being dragged with the mouse use the position that was set in MouseMove + if !self.is_dragging { + self.knob_position.x = if self.is_on { ON_POS } else { OFF_POS } + } + + // Paint the Swith background + // The on/off states have different colors + // The transition between the two color is controlled by the knob position and calculated using the opacity + let opacity = (self.knob_position.x - OFF_POS) / (ON_POS - OFF_POS); + + let background_on_state = Color::SPRING_GREEN.with_alpha_factor(opacity as f32); + let background_off_state = Color::WHITE_SMOKE.with_alpha_factor(1.0 - opacity as f32); + + let background_rect = cx.size().to_rect().to_rounded_rect(SWITCH_HEIGHT / 2.); + + fill_color(builder, &background_rect, background_off_state); + fill_color(builder, &background_rect, background_on_state); + + // Paint the Switch knob + println!("Paint: {:?}", self.knob_position); + let knob_color = if self.is_dragging || cx.is_hot() { + Color::SLATE_GRAY + } else { + Color::LIGHT_SLATE_GRAY + }; + let knob_border_color = Color::DIM_GRAY; + let mut knob_size = KNOB_DIAMETER / 2.0; + + if cx.is_active() { + knob_size += 1.0; + } + + let knob_circle = Circle::new(self.knob_position, knob_size); + fill_color(builder, &knob_circle, knob_color); + stroke(builder, &knob_circle, knob_border_color, 2.0); + } +}