Rename to xilem

I've decided on the name "xilem" rather than "idiopath," and this also matches the blog post (now linked from the README, though that is a bit stale).
This commit is contained in:
Raph Levien 2022-05-19 09:01:39 -07:00
commit 26432cebf8
26 changed files with 2778 additions and 0 deletions

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "xilem"
version = "0.1.0"
license = "Apache-2.0"
authors = ["Raph Levien <raph@google.com>"]
edition = "2021"
[dependencies]
"druid-shell" = { path = "../druid-shell" }
bitflags = "1.3.2"

80
README.md Normal file
View File

@ -0,0 +1,80 @@
# An experimental Rust architecture for reactive UI
Note: this README is a bit out of date. To understand more of what's going on, please read the blog post, [Xilem: an architecture for UI in Rust].
This repo contains an experimental architecture, implemented with a toy UI. At a very high level, it combines ideas from Flutter, SwiftUI, and Elm. Like all of these, it uses lightweight view objects, diffing them to provide minimal updates to a retained UI. Like SwiftUI, it is strongly typed.
## Overall program flow
Like Elm, the app logic contains *centralized state.* On each cycle (meaning, roughly, on each high-level UI interaction such as a button click), the framework calls a closure, giving it mutable access to the app state, and the return value is a *view tree.* This view tree is fairly short-lived; it is used to render the UI, possibly dispatch some events, and be used as a reference for *diffing* by the next cycle, at which point it is dropped.
We'll use the standard counter example. Here the state is a single integer, and the view tree is a column containing two buttons.
```rust
fn app_logic(data: &mut u32) -> impl View<u32, (), Element = impl Widget> {
Column::new((
Button::new(format!("count: {}", data), |data| *data += 1),
Button::new("reset", |data| *data = 0),
))
}
```
These are all just vanilla data structures. The next step is diffing or reconciling against a previous version, now a standard technique. The result is an *element tree.* Each node type in the view tree has a corresponding element as an associated type. The `build` method on a view node creates the element, and the `rebuild` method diffs against the previous version (for example, if the string changes) and updates the element. There's also an associated state tree, not actually needed in this simple example, but would be used for memoization.
The closures are the interesting part. When they're run, they take a mutable reference to the app data.
## Components
A major goal is to support React-like components, where modules that build UI for some fragment of the overall app state are composed together.
```rust
struct AppData {
count: u32,
}
fn count_button(count: u32) -> impl View<u32, (), Element = impl Widget> {
Button::new(format!("count: {}", count), |data| *data += 1)
}
fn app_logic(data: &mut AppData) -> impl View<AppData, (), Element = impl Widget> {
Adapt::new(|data: &mut AppData, thunk| thunk.call(&mut data.count),
count_button(data.count))
}
```
This adapt node is very similar to a lens (quite familiar to existing Druid users), and is also very similar to the [Html.map] node in Elm. Note that in this case the data presented to the child component to render, and the mutable app state available in callbacks is the same, but that is not necessarily the case.
## Memoization
In the simplest case, the app builds the entire view tree, which is diffed against the previous tree, only to find that most of it hasn't changed.
When a subtree is a pure function of some data, as is the case for the button above, it makes sense to *memoize.* The data is compared to the previous version, and only when it's changed is the view tree build. The signature of the memoize node is nearly identical to [Html.lazy] in Elm:
```rust
fn app_logic(data: &mut AppData) -> impl View<AppData, (), Element = impl Widget> {
Memoize::new(data.count, |count| {
Button::new(format!("count: {}", count), |data: &mut AppData| {
data.count += 1
})
}),
}
```
The current code uses a `PartialEq` bound, but in practice I think it might be much more useful to use pointer equality on `Rc` and `Arc`.
The combination of memoization with pointer equality and an adapt node that calls [Rc::make_mut] on the parent type is actually a powerful form of change tracking, similar in scope to Adapton, self-adjusting computation, or the types of binding objects used in SwiftUI. If a piece of data is rendered in two different places, it automatically propagates the change to both of those, without having to do any explicit management of the dependency graph.
I anticipate it will also be possible to do dirty tracking manually - the app logic can set a dirty flag when a subtree needs re-rendering.
## Optional type erasure
By default, view nodes are strongly typed. The type of a container includes the types of its children (through the `ViewTuple` trait), so for a large tree the type can become quite large. In addition, such types don't make for easy dynamic reconfiguration of the UI. SwiftUI has exactly this issue, and provides [AnyView] as the solution. Ours is more or less identical.
The type erasure of View nodes is not an easy trick, as the trait has two associated types and the `rebuild` method takes the previous view as a `&Self` typed parameter. Nonetheless, it is possible. (As far as I know, Olivier Faure was the first to demonstrate this technique, in [Panoramix], but I'm happy to be further enlightened)
[Html.lazy]: https://guide.elm-lang.org/optimization/lazy.html
[Html map]: https://package.elm-lang.org/packages/elm/html/latest/Html#map
[Rc::make_mut]: https://doc.rust-lang.org/std/rc/struct.Rc.html#method.make_mut
[AnyView]: https://developer.apple.com/documentation/swiftui/anyview
[Panoramix]: https://github.com/PoignardAzur/panoramix
[Xilem: an architecture for UI in Rust]: https://raphlinus.github.io/rust/gui/2022/05/07/ui-architecture.html

46
examples/counter.rs Normal file
View File

@ -0,0 +1,46 @@
// 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 xilem::{button, v_stack, Adapt, App, AppLauncher, LayoutObserver, Memoize, View};
#[derive(Default)]
struct AppData {
count: u32,
}
fn count_button(count: u32) -> impl View<u32> {
button(format!("count: {}", count), |data| *data += 1)
}
fn app_logic(data: &mut AppData) -> impl View<AppData> {
v_stack((
format!("count: {}", data.count),
button("reset", |data: &mut AppData| data.count = 0),
Memoize::new(data.count, |count| {
button(format!("count: {}", count), |data: &mut AppData| {
data.count += 1
})
}),
Adapt::new(
|data: &mut AppData, thunk| thunk.call(&mut data.count),
count_button(data.count),
),
LayoutObserver::new(|size| format!("size: {:?}", size)),
))
}
pub fn main() {
let app = App::new(AppData::default(), app_logic);
AppLauncher::new(app).run();
}

152
src/app.rs Normal file
View File

@ -0,0 +1,152 @@
// 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 druid_shell::kurbo::Size;
use druid_shell::piet::{Color, Piet, RenderContext};
use druid_shell::{kurbo::Point, WindowHandle};
use crate::widget::{CxState, EventCx, LayoutCx, PaintCx, Pod, UpdateCx, WidgetState};
use crate::{
event::Event,
id::Id,
view::{Cx, View},
widget::{RawEvent, Widget},
};
pub struct App<T, V: View<T>, F: FnMut(&mut T) -> V> {
data: T,
app_logic: F,
view: Option<V>,
id: Option<Id>,
state: Option<V::State>,
events: Vec<Event>,
window_handle: WindowHandle,
root_state: WidgetState,
root_pod: Option<Pod>,
size: Size,
cx: Cx,
}
const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22);
impl<T, V: View<T>, F: FnMut(&mut T) -> V> App<T, V, F>
where
V::Element: Widget + 'static,
{
pub fn new(data: T, app_logic: F) -> Self {
let cx = Cx::new();
App {
data,
app_logic,
view: None,
id: None,
state: None,
root_pod: None,
events: Vec::new(),
window_handle: Default::default(),
root_state: Default::default(),
size: Default::default(),
cx,
}
}
pub fn ensure_app(&mut self) {
if self.view.is_none() {
let view = (self.app_logic)(&mut self.data);
let (id, state, element) = view.build(&mut self.cx);
let root_pod = Pod::new(element);
self.view = Some(view);
self.id = Some(id);
self.state = Some(state);
self.root_pod = Some(root_pod);
}
}
pub fn connect(&mut self, window_handle: WindowHandle) {
self.window_handle = window_handle.clone();
// This will be needed for wiring up async but is a stub for now.
//self.cx.set_handle(window_handle.get_idle_handle());
}
pub fn size(&mut self, size: Size) {
self.size = size;
}
pub fn paint(&mut self, piet: &mut Piet) {
let rect = self.size.to_rect();
piet.fill(rect, &BG_COLOR);
self.ensure_app();
loop {
let root_pod = self.root_pod.as_mut().unwrap();
let mut cx_state = CxState::new(&self.window_handle, &mut self.events);
let mut update_cx = UpdateCx::new(&mut cx_state, &mut self.root_state);
root_pod.update(&mut update_cx);
let mut layout_cx = LayoutCx::new(&mut cx_state, &mut self.root_state);
root_pod.prelayout(&mut layout_cx);
let proposed_size = self.size;
root_pod.layout(&mut layout_cx, proposed_size);
if cx_state.has_events() {
// We might want some debugging here if the number of iterations
// becomes extreme.
self.run_app_logic();
} else {
let mut paint_cx = PaintCx::new(&mut cx_state, piet);
root_pod.paint(&mut paint_cx);
break;
}
}
}
pub fn mouse_down(&mut self, point: Point) {
self.event(RawEvent::MouseDown(point));
}
fn event(&mut self, event: RawEvent) {
self.ensure_app();
let root_pod = self.root_pod.as_mut().unwrap();
let mut cx_state = CxState::new(&self.window_handle, &mut self.events);
let mut event_cx = EventCx::new(&mut cx_state, &mut self.root_state);
root_pod.event(&mut event_cx, &event);
self.run_app_logic();
}
pub fn run_app_logic(&mut self) {
for event in self.events.drain(..) {
let id_path = &event.id_path[1..];
self.view.as_ref().unwrap().event(
id_path,
self.state.as_mut().unwrap(),
event.body,
&mut self.data,
);
}
// Re-rendering should be more lazy.
let view = (self.app_logic)(&mut self.data);
if let Some(element) = self.root_pod.as_mut().unwrap().downcast_mut() {
let changed = view.rebuild(
&mut self.cx,
self.view.as_ref().unwrap(),
self.id.as_mut().unwrap(),
self.state.as_mut().unwrap(),
element,
);
if changed {
self.root_pod.as_mut().unwrap().request_update();
}
assert!(self.cx.is_empty(), "id path imbalance on rebuild");
}
self.view = Some(view);
}
}

143
src/app_main.rs Normal file
View File

@ -0,0 +1,143 @@
// 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 druid_shell::{
kurbo::Size, Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler,
WindowBuilder, WindowHandle,
};
use crate::{app::App, View, Widget};
// This is a bit of a hack just to get a window launched. The real version
// would deal with multiple windows and have other ways to configure things.
pub struct AppLauncher<T, V: View<T>, F: FnMut(&mut T) -> V> {
title: String,
app: App<T, V, F>,
}
struct MainState<T, V: View<T>, F: FnMut(&mut T) -> V>
where
V::Element: Widget,
{
handle: WindowHandle,
app: App<T, V, F>,
}
const QUIT_MENU_ID: u32 = 0x100;
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> AppLauncher<T, V, F> {
pub fn new(app: App<T, V, F>) -> Self {
AppLauncher {
title: "Xilem app".into(),
app,
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn run(self) {
let mut file_menu = Menu::new();
file_menu.add_item(
QUIT_MENU_ID,
"E&xit",
Some(&HotKey::new(SysMods::Cmd, "q")),
true,
false,
);
let mut menubar = Menu::new();
menubar.add_dropdown(Menu::new(), "Application", true);
menubar.add_dropdown(file_menu, "&File", true);
let druid_app = Application::new().unwrap();
let mut builder = WindowBuilder::new(druid_app.clone());
let main_state = MainState::new(self.app);
builder.set_handler(Box::new(main_state));
builder.set_title(self.title);
builder.set_menu(menubar);
let window = builder.build().unwrap();
window.show();
druid_app.run(None);
}
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> WinHandler
for MainState<T, V, F>
where
V::Element: Widget,
{
fn connect(&mut self, handle: &WindowHandle) {
self.handle = handle.clone();
self.app.connect(handle.clone());
}
fn prepare_paint(&mut self) {}
fn paint(&mut self, piet: &mut druid_shell::piet::Piet, _: &Region) {
self.app.paint(piet);
}
fn command(&mut self, id: u32) {
match id {
QUIT_MENU_ID => {
self.handle.close();
Application::global().quit()
}
_ => println!("unexpected id {}", id),
}
}
fn mouse_move(&mut self, _event: &MouseEvent) {
self.handle.set_cursor(&Cursor::Arrow);
}
fn mouse_down(&mut self, event: &MouseEvent) {
self.app.mouse_down(event.pos);
self.handle.invalidate();
}
fn mouse_up(&mut self, _event: &MouseEvent) {}
fn size(&mut self, size: Size) {
self.app.size(size);
}
fn request_close(&mut self) {
self.handle.close();
}
fn destroy(&mut self) {
Application::global().quit()
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
}
impl<T, V: View<T>, F: FnMut(&mut T) -> V> MainState<T, V, F>
where
V::Element: Widget,
{
fn new(app: App<T, V, F>) -> Self {
let state = MainState {
handle: Default::default(),
app,
};
state
}
}

60
src/event.rs Normal file
View File

@ -0,0 +1,60 @@
// 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::id::IdPath;
pub struct Event {
pub id_path: IdPath,
pub body: Box<dyn Any>,
}
/// A result wrapper type for event handlers.
pub enum EventResult<A> {
/// The event handler was invoked and returned an action.
Action(A),
/// The event handler received a change request that requests a rebuild.
#[allow(unused)]
RequestRebuild,
/// The event handler discarded the event.
#[allow(unused)]
Nop,
/// The event was addressed to an id path no longer in the tree.
///
/// This is a normal outcome for async operation when the tree is changing
/// dynamically, but otherwise indicates a logic error.
Stale,
}
impl<A> EventResult<A> {
#[allow(unused)]
pub fn map<B>(self, f: impl FnOnce(A) -> B) -> EventResult<B> {
match self {
EventResult::Action(a) => EventResult::Action(f(a)),
EventResult::RequestRebuild => EventResult::RequestRebuild,
EventResult::Stale => EventResult::Stale,
EventResult::Nop => EventResult::Nop,
}
}
}
impl Event {
pub fn new(id_path: IdPath, event: impl Any) -> Event {
Event {
id_path,
body: Box::new(event),
}
}
}

35
src/id.rs Normal file
View File

@ -0,0 +1,35 @@
// 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::num::NonZeroU64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)]
/// A stable identifier for an element.
pub struct Id(NonZeroU64);
pub type IdPath = Vec<Id>;
impl Id {
/// Allocate a new, unique `Id`.
pub fn next() -> Id {
use druid_shell::Counter;
static WIDGET_ID_COUNTER: Counter = Counter::new();
Id(WIDGET_ID_COUNTER.next_nonzero())
}
#[allow(unused)]
pub fn to_raw(self) -> u64 {
self.0.into()
}
}

37
src/lib.rs Normal file
View File

@ -0,0 +1,37 @@
// 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.
//! Prototype implementation of Xilem architecture.
//!
//! This is a skeletal, proof-of-concept UI toolkit to prove out the Xilem
//! architectural ideas.
mod app;
mod app_main;
mod event;
mod id;
mod view;
mod view_seq;
mod widget;
pub use app::App;
pub use app_main::AppLauncher;
pub use view::adapt::Adapt;
pub use view::button::button;
pub use view::layout_observer::LayoutObserver;
pub use view::memoize::Memoize;
pub use view::vstack::v_stack;
pub use view::View;
pub use widget::align::{AlignmentAxis, AlignmentProxy, HorizAlignment, VertAlignment};
pub use widget::Widget;

130
src/view.rs Normal file
View File

@ -0,0 +1,130 @@
// 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.
pub mod adapt;
pub mod any_view;
pub mod button;
pub mod layout_observer;
pub mod memoize;
pub mod text;
pub mod use_state;
pub mod vstack;
use std::any::Any;
use crate::{
event::EventResult,
id::{Id, IdPath},
widget::Widget,
};
/// A view object representing a node in the UI.
///
/// This is a central trait for representing UI. An app will generate a tree of
/// these objects (the view tree) as the primary interface for expressing UI.
/// The view tree is transitory and is retained only long enough to dispatch
/// events and then serve as a reference for diffing for the next view tree.
///
/// The framework will then run methods on these views to create the associated
/// state tree and widget tree, as well as incremental updates and event
/// propagation.
///
/// The `View` trait is parameterized by `T`, which is known as the "app state",
/// and also a type for actions which are passed up the tree in event
/// propagation. During event handling, mutable access to the app state is
/// given to view nodes, which in turn can make expose it to callbacks.
pub trait View<T, A = ()> {
/// Associated state for the view.
type State;
/// The associated widget for the view.
type Element: Widget;
/// Build the associated widget and initialize state.
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element);
/// Update the associated widget.
///
/// Returns `true` when anything has changed.
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool;
/// Propagate an event.
///
/// Handle an event, propagating to children if needed. Here, `id_path` is a slice
/// of ids beginning at a child of this view.
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A>;
}
#[derive(Clone)]
pub struct Cx {
id_path: IdPath,
}
impl Cx {
pub fn new() -> Self {
Cx {
id_path: Vec::new(),
}
}
pub fn push(&mut self, id: Id) {
self.id_path.push(id);
}
pub fn pop(&mut self) {
self.id_path.pop();
}
pub fn is_empty(&self) -> bool {
self.id_path.is_empty()
}
pub fn id_path(&self) -> &IdPath {
&self.id_path
}
/// Run some logic with an id added to the id path.
///
/// This is an ergonomic helper that ensures proper nesting of the id path.
pub fn with_id<T, F: FnOnce(&mut Cx) -> T>(&mut self, id: Id, f: F) -> T {
self.push(id);
let result = f(self);
self.pop();
result
}
/// Allocate a new id and run logic with the new id added to the id path.
///
/// Also an ergonomic helper.
pub fn with_new_id<T, F: FnOnce(&mut Cx) -> T>(&mut self, f: F) -> (Id, T) {
let id = Id::next();
self.push(id);
let result = f(self);
self.pop();
(id, result)
}
}

94
src/view/adapt.rs Normal file
View File

@ -0,0 +1,94 @@
// 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, marker::PhantomData};
use crate::{event::EventResult, id::Id};
use super::{Cx, View};
pub struct Adapt<T, A, U, B, F: Fn(&mut T, AdaptThunk<U, B, C>) -> EventResult<A>, C: View<U, B>> {
f: F,
child: C,
phantom: PhantomData<(T, A, U, B)>,
}
/// A "thunk" which dispatches an event to an adapt node's child.
///
/// The closure passed to Adapt should call this thunk with the child's
/// app state.
pub struct AdaptThunk<'a, U, B, C: View<U, B>> {
child: &'a C,
state: &'a mut C::State,
id_path: &'a [Id],
event: Box<dyn Any>,
}
impl<T, A, U, B, F: Fn(&mut T, AdaptThunk<U, B, C>) -> EventResult<A>, C: View<U, B>>
Adapt<T, A, U, B, F, C>
{
pub fn new(f: F, child: C) -> Self {
Adapt {
f,
child,
phantom: Default::default(),
}
}
}
impl<'a, U, B, C: View<U, B>> AdaptThunk<'a, U, B, C> {
pub fn call(self, app_state: &mut U) -> EventResult<B> {
self.child
.event(self.id_path, self.state, self.event, app_state)
}
}
impl<T, A, U, B, F: Fn(&mut T, AdaptThunk<U, B, C>) -> EventResult<A>, C: View<U, B>> View<T, A>
for Adapt<T, A, U, B, F, C>
{
type State = C::State;
type Element = C::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
self.child.build(cx)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
self.child.rebuild(cx, &prev.child, id, state, element)
}
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
let thunk = AdaptThunk {
child: &self.child,
state,
id_path,
event,
};
(self.f)(app_state, thunk)
}
}

145
src/view/any_view.rs Normal file
View File

@ -0,0 +1,145 @@
//
// 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,
ops::{Deref, DerefMut},
};
use crate::{event::EventResult, id::Id, widget::AnyWidget};
use super::{Cx, View};
/// A trait enabling type erasure of views.
///
/// The name is slightly misleading as it's not any view, but only ones
/// whose element is AnyWidget.
///
/// Making a trait which is generic over another trait bound appears to
/// be well beyond the capability of Rust's type system. If type-erased
/// views with other bounds are needed, the best approach is probably
/// duplication of the code, probably with a macro.
pub trait AnyView<T, A = ()> {
fn as_any(&self) -> &dyn Any;
fn dyn_build(&self, cx: &mut Cx) -> (Id, Box<dyn Any>, Box<dyn AnyWidget>);
fn dyn_rebuild(
&self,
cx: &mut Cx,
prev: &dyn AnyView<T, A>,
id: &mut Id,
state: &mut Box<dyn Any>,
element: &mut Box<dyn AnyWidget>,
) -> bool;
fn dyn_event(
&self,
id_path: &[Id],
state: &mut dyn Any,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A>;
}
impl<T, A, V: View<T, A> + 'static> AnyView<T, A> for V
where
V::State: 'static,
V::Element: AnyWidget + 'static,
{
fn as_any(&self) -> &dyn Any {
self
}
fn dyn_build(&self, cx: &mut Cx) -> (Id, Box<dyn Any>, Box<dyn AnyWidget>) {
let (id, state, element) = self.build(cx);
(id, Box::new(state), Box::new(element))
}
fn dyn_rebuild(
&self,
cx: &mut Cx,
prev: &dyn AnyView<T, A>,
id: &mut Id,
state: &mut Box<dyn Any>,
element: &mut Box<dyn AnyWidget>,
) -> bool {
if let Some(prev) = prev.as_any().downcast_ref() {
if let Some(state) = state.downcast_mut() {
if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() {
self.rebuild(cx, prev, id, state, element)
} else {
println!("downcast of element failed in dyn_rebuild");
false
}
} else {
println!("downcast of state failed in dyn_rebuild");
false
}
} else {
let (new_id, new_state, new_element) = self.build(cx);
*id = new_id;
*state = Box::new(new_state);
*element = Box::new(new_element);
true
}
}
fn dyn_event(
&self,
id_path: &[Id],
state: &mut dyn Any,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
if let Some(state) = state.downcast_mut() {
self.event(id_path, state, event, app_state)
} else {
// Possibly softer failure?
panic!("downcast error in dyn_event");
}
}
}
impl<T, A> View<T, A> for Box<dyn AnyView<T, A>> {
type State = Box<dyn Any>;
type Element = Box<dyn AnyWidget>;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
self.deref().dyn_build(cx)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
self.deref()
.dyn_rebuild(cx, prev.deref(), id, state, element)
}
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
self.deref()
.dyn_event(id_path, state.deref_mut(), event, app_state)
}
}

79
src/view/button.rs Normal file
View File

@ -0,0 +1,79 @@
// 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::{event::EventResult, id::Id};
use super::{Cx, View};
pub struct Button<T, A> {
label: String,
// consider not boxing
callback: Box<dyn Fn(&mut T) -> A>,
}
pub fn button<T, A>(
label: impl Into<String>,
clicked: impl Fn(&mut T) -> A + 'static,
) -> Button<T, A> {
Button::new(label, clicked)
}
impl<T, A> Button<T, A> {
pub fn new(label: impl Into<String>, clicked: impl Fn(&mut T) -> A + 'static) -> Self {
Button {
label: label.into(),
callback: Box::new(clicked),
}
}
}
impl<T, A> View<T, A> for Button<T, A> {
type State = ();
type Element = crate::widget::button::Button;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, element) = cx
.with_new_id(|cx| crate::widget::button::Button::new(cx.id_path(), self.label.clone()));
(id, (), element)
}
fn rebuild(
&self,
_cx: &mut Cx,
prev: &Self,
_id: &mut crate::id::Id,
_state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
if prev.label != self.label {
element.set_label(self.label.clone());
true
} else {
false
}
}
fn event(
&self,
_id_path: &[crate::id::Id],
_state: &mut Self::State,
_event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
EventResult::Action((self.callback)(app_state))
}
}

125
src/view/layout_observer.rs Normal file
View File

@ -0,0 +1,125 @@
use std::{any::Any, marker::PhantomData};
// 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 druid_shell::kurbo::Size;
use crate::{event::EventResult, id::Id};
use super::{Cx, View};
pub struct LayoutObserver<T, A, F, V> {
callback: F,
phantom: PhantomData<(T, A, V)>,
}
pub struct LayoutObserverState<T, A, V: View<T, A>> {
size: Option<Size>,
child_id: Option<Id>,
child_view: Option<V>,
child_state: Option<V::State>,
}
impl<T, A, F, V> LayoutObserver<T, A, F, V> {
pub fn new(callback: F) -> Self {
LayoutObserver {
callback,
phantom: Default::default(),
}
}
}
impl<T, A, F: Fn(Size) -> V, V: View<T, A>> View<T, A> for LayoutObserver<T, A, F, V>
where
V::Element: 'static,
{
type State = LayoutObserverState<T, A, V>;
type Element = crate::widget::layout_observer::LayoutObserver;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, element) =
cx.with_new_id(|cx| crate::widget::layout_observer::LayoutObserver::new(cx.id_path()));
let child_state = LayoutObserverState {
size: None,
child_id: None,
child_view: None,
child_state: None,
};
(id, child_state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
_prev: &Self,
id: &mut crate::id::Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
if let Some(size) = &state.size {
let view = (self.callback)(*size);
cx.with_id(*id, |cx| {
if let (Some(id), Some(prev_view), Some(child_state)) = (
&mut state.child_id,
&state.child_view,
&mut state.child_state,
) {
let child_pod = element.child_mut().as_mut().unwrap();
let child_element = child_pod.downcast_mut().unwrap();
let changed = view.rebuild(cx, prev_view, id, child_state, child_element);
state.child_view = Some(view);
if changed {
child_pod.request_update();
}
changed
} else {
let (child_id, child_state, child_element) = view.build(cx);
element.set_child(Box::new(child_element));
state.child_id = Some(child_id);
state.child_state = Some(child_state);
state.child_view = Some(view);
true
}
})
} else {
false
}
}
fn event(
&self,
id_path: &[crate::id::Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
if id_path.is_empty() {
if let Ok(size) = event.downcast() {
state.size = Some(*size);
}
EventResult::RequestRebuild
} else {
let tl = &id_path[1..];
if let (Some(child_view), Some(child_state)) =
(&state.child_view, &mut state.child_state)
{
child_view.event(tl, child_state, event, app_state)
} else {
EventResult::Stale
}
}
}
}

89
src/view/memoize.rs Normal file
View File

@ -0,0 +1,89 @@
// 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::{event::EventResult, id::Id};
use super::{Cx, View};
pub struct Memoize<D, F> {
data: D,
child_cb: F,
}
pub struct MemoizeState<T, A, V: View<T, A>> {
view: V,
view_state: V::State,
dirty: bool,
}
impl<D, V, F: Fn(&D) -> V> Memoize<D, F> {
pub fn new(data: D, child_cb: F) -> Self {
Memoize { data, child_cb }
}
}
impl<T, A, D: PartialEq + Clone + 'static, V: View<T, A>, F: Fn(&D) -> V> View<T, A>
for Memoize<D, F>
{
type State = MemoizeState<T, A, V>;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let view = (self.child_cb)(&self.data);
let (id, view_state, element) = view.build(cx);
let memoize_state = MemoizeState {
view,
view_state,
dirty: false,
};
(id, memoize_state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
if std::mem::take(&mut state.dirty) || prev.data != self.data {
let view = (self.child_cb)(&self.data);
let changed = view.rebuild(cx, &state.view, id, &mut state.view_state, element);
state.view = view;
changed
} else {
false
}
}
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
let r = state
.view
.event(id_path, &mut state.view_state, event, app_state);
if matches!(r, EventResult::RequestRebuild) {
state.dirty = true;
}
r
}
}

56
src/view/text.rs Normal file
View File

@ -0,0 +1,56 @@
// 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::{event::EventResult, id::Id};
use super::{Cx, View};
impl<T, A> View<T, A> for String {
type State = ();
type Element = crate::widget::text::TextWidget;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, element) = cx.with_new_id(|_| crate::widget::text::TextWidget::new(self.clone()));
(id, (), element)
}
fn rebuild(
&self,
_cx: &mut Cx,
prev: &Self,
_id: &mut crate::id::Id,
_state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
if prev != self {
element.set_text(self.clone());
true
} else {
false
}
}
fn event(
&self,
_id_path: &[crate::id::Id],
_state: &mut Self::State,
_event: Box<dyn Any>,
_app_state: &mut T,
) -> EventResult<A> {
EventResult::Stale
}
}

101
src/view/use_state.rs Normal file
View File

@ -0,0 +1,101 @@
// 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, marker::PhantomData, rc::Rc};
use crate::{event::EventResult, id::Id};
use super::{Cx, View};
/// An implementation of the "use_state" pattern familiar in reactive UI.
///
/// This may not be the final form. In this version, the parent app data
/// is `Rc<T>`, and the child is `(Rc<T>, S)` where S is the local state.
///
/// The first callback creates the initial state (it is called on build but
/// not rebuild). The second callback takes that state as an argument. It
/// is not passed the app state, but since that state is `Rc`, it would be
/// natural to clone it and capture it in a `move` closure.
pub struct UseState<T, A, S, V, FInit, F> {
f_init: FInit,
f: F,
phantom: PhantomData<(T, A, S, V)>,
}
pub struct UseStateState<T, A, S, V: View<(Rc<T>, S), A>> {
state: Option<S>,
view: V,
view_state: V::State,
}
impl<T, A, S, V, FInit: Fn() -> S, F: Fn(&mut S) -> V> UseState<T, A, S, V, FInit, F> {
#[allow(unused)]
pub fn new(f_init: FInit, f: F) -> Self {
let phantom = Default::default();
UseState { f_init, f, phantom }
}
}
impl<T, A, S, V: View<(Rc<T>, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View<Rc<T>, A>
for UseState<T, A, S, V, FInit, F>
{
type State = UseStateState<T, A, S, V>;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let mut state = (self.f_init)();
let view = (self.f)(&mut state);
let (id, view_state, element) = view.build(cx);
let my_state = UseStateState {
state: Some(state),
view,
view_state,
};
(id, my_state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
_prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
let view = (self.f)(state.state.as_mut().unwrap());
let changed = view.rebuild(cx, &state.view, id, &mut state.view_state, element);
state.view = view;
changed
}
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut Rc<T>,
) -> EventResult<A> {
let mut local_state = (app_state.clone(), state.state.take().unwrap());
let a = state
.view
.event(id_path, &mut state.view_state, event, &mut local_state);
let (local_app_state, my_state) = local_state;
if !Rc::ptr_eq(app_state, &local_app_state) {
*app_state = local_app_state
}
state.state = Some(my_state);
a
}
}

74
src/view/vstack.rs Normal file
View File

@ -0,0 +1,74 @@
// 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, marker::PhantomData};
use crate::{event::EventResult, id::Id, view_seq::ViewSequence, widget::WidgetTuple};
use super::{Cx, View};
pub struct VStack<T, A, VT: ViewSequence<T, A>> {
children: VT,
phantom: PhantomData<(T, A)>,
}
pub fn v_stack<T, A, VT: ViewSequence<T, A>>(children: VT) -> VStack<T, A, VT> {
VStack::new(children)
}
impl<T, A, VT: ViewSequence<T, A>> VStack<T, A, VT> {
pub fn new(children: VT) -> Self {
let phantom = Default::default();
VStack { children, phantom }
}
}
impl<T, A, VT: ViewSequence<T, A>> View<T, A> for VStack<T, A, VT>
where
VT::Elements: WidgetTuple,
{
type State = VT::State;
type Element = crate::widget::vstack::VStack;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, (state, elements)) = cx.with_new_id(|cx| self.children.build(cx));
let column = crate::widget::vstack::VStack::new(elements);
(id, state, column)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> bool {
cx.with_id(*id, |cx| {
self.children
.rebuild(cx, &prev.children, state, element.children_mut())
})
}
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
self.children.event(id_path, state, event, app_state)
}
}

122
src/view_seq.rs Normal file
View File

@ -0,0 +1,122 @@
// 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::{
event::EventResult,
id::Id,
view::{Cx, View},
widget::Pod,
};
pub trait ViewSequence<T, A> {
type State;
type Elements;
fn build(&self, cx: &mut Cx) -> (Self::State, Vec<Pod>);
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
state: &mut Self::State,
els: &mut Vec<Pod>,
) -> bool;
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A>;
}
macro_rules! impl_view_tuple {
( $n: tt; $( $t:ident),* ; $( $i:tt ),* ) => {
impl<T, A, $( $t: View<T, A> ),* > ViewSequence<T, A> for ( $( $t, )* )
where $( <$t as View<T, A>>::Element: 'static ),*
{
type State = ( $( $t::State, )* [Id; $n]);
type Elements = ( $( $t::Element, )* );
fn build(&self, cx: &mut Cx) -> (Self::State, Vec<Pod>) {
let b = ( $( self.$i.build(cx), )* );
let state = ( $( b.$i.1, )* [ $( b.$i.0 ),* ]);
let els = vec![ $( Pod::new(b.$i.2) ),* ];
(state, els)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
state: &mut Self::State,
els: &mut Vec<Pod>,
) -> bool {
let mut changed = false;
$(
if self.$i
.rebuild(cx, &prev.$i, &mut state.$n[$i], &mut state.$i,
els[$i].downcast_mut().unwrap())
{
els[$i].request_update();
changed = true;
}
)*
changed
}
fn event(
&self,
id_path: &[Id],
state: &mut Self::State,
event: Box<dyn Any>,
app_state: &mut T,
) -> EventResult<A> {
let hd = id_path[0];
let tl = &id_path[1..];
$(
if hd == state.$n[$i] {
self.$i.event(tl, &mut state.$i, event, app_state)
} else )* {
crate::event::EventResult::Stale
}
}
}
}
}
impl_view_tuple!(1; V0; 0);
impl_view_tuple!(2; V0, V1; 0, 1);
impl_view_tuple!(3; V0, V1, V2; 0, 1, 2);
impl_view_tuple!(4; V0, V1, V2, V3; 0, 1, 2, 3);
impl_view_tuple!(5; V0, V1, V2, V3, V4; 0, 1, 2, 3, 4);
impl_view_tuple!(6; V0, V1, V2, V3, V4, V5; 0, 1, 2, 3, 4, 5);
impl_view_tuple!(7; V0, V1, V2, V3, V4, V5, V6; 0, 1, 2, 3, 4, 5, 6);
impl_view_tuple!(8;
V0, V1, V2, V3, V4, V5, V6, V7;
0, 1, 2, 3, 4, 5, 6, 7
);
impl_view_tuple!(9;
V0, V1, V2, V3, V4, V5, V6, V7, V8;
0, 1, 2, 3, 4, 5, 6, 7, 8
);
impl_view_tuple!(10;
V0, V1, V2, V3, V4, V5, V6, V7, V8, V9;
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
);

141
src/widget.rs Normal file
View File

@ -0,0 +1,141 @@
// 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.
pub mod align;
pub mod button;
mod contexts;
mod core;
pub mod layout_observer;
pub mod text;
pub mod vstack;
use std::any::Any;
use std::ops::DerefMut;
use druid_shell::kurbo::{Point, Size};
pub use self::contexts::{AlignCx, CxState, EventCx, LayoutCx, PaintCx, UpdateCx};
pub use self::core::Pod;
pub(crate) use self::core::{PodFlags, WidgetState};
use self::align::SingleAlignment;
/// A basic widget trait.
pub trait Widget {
fn event(&mut self, cx: &mut EventCx, event: &RawEvent);
fn update(&mut self, cx: &mut UpdateCx);
/// Compute intrinsic sizes.
///
/// This method will be called once on widget creation and then on
/// REQUEST_UPDATE.
fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size);
/// Compute size given proposed size.
///
/// The value will be memoized given the proposed size, invalidated
/// on REQUEST_UPDATE. It can count on prelayout being completed.
fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size;
/// Query for an alignment.
///
/// This method can count on layout already having been completed.
#[allow(unused)]
fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {}
fn paint(&mut self, cx: &mut PaintCx);
}
pub trait AnyWidget: Widget {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<W: Widget + 'static> AnyWidget for W {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Widget for Box<dyn AnyWidget> {
fn event(&mut self, cx: &mut EventCx, event: &RawEvent) {
self.deref_mut().event(cx, event);
}
fn update(&mut self, cx: &mut UpdateCx) {
self.deref_mut().update(cx);
}
fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
self.deref_mut().prelayout(cx)
}
fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size {
self.deref_mut().layout(cx, proposed_size)
}
fn paint(&mut self, cx: &mut PaintCx) {
self.deref_mut().paint(cx);
}
}
#[derive(Debug)]
pub enum RawEvent {
MouseDown(Point),
}
pub trait WidgetTuple {
fn length(&self) -> usize;
// Follows Panoramix; rethink to reduce allocation
// Maybe SmallVec?
fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget>;
}
macro_rules! impl_widget_tuple {
( $n: tt; $( $WidgetType:ident),* ; $( $index:tt ),* ) => {
impl< $( $WidgetType: AnyWidget ),* > WidgetTuple for ( $( $WidgetType, )* ) {
fn length(&self) -> usize {
$n
}
fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget> {
let mut v: Vec<&mut dyn AnyWidget> = Vec::with_capacity(self.length());
$(
v.push(&mut self.$index);
)*
v
}
}
}
}
impl_widget_tuple!(1; W0; 0);
impl_widget_tuple!(2; W0, W1; 0, 1);
impl_widget_tuple!(3; W0, W1, W2; 0, 1, 2);
impl_widget_tuple!(4; W0, W1, W2, W3; 0, 1, 2, 3);
impl_widget_tuple!(5; W0, W1, W2, W3, W4; 0, 1, 2, 3, 4);
impl_widget_tuple!(6; W0, W1, W2, W3, W4, W5; 0, 1, 2, 3, 4, 5);
impl_widget_tuple!(7; W0, W1, W2, W3, W4, W5, W6; 0, 1, 2, 3, 4, 5, 6);
impl_widget_tuple!(8;
W0, W1, W2, W3, W4, W5, W6, W7;
0, 1, 2, 3, 4, 5, 6, 7
);

283
src/widget/align.rs Normal file
View File

@ -0,0 +1,283 @@
// 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 druid_shell::kurbo::Point;
use super::{AlignCx, AnyWidget, EventCx, Widget, WidgetState};
#[derive(Clone, Copy, PartialEq)]
pub enum AlignmentMerge {
Min,
Mean,
Max,
}
#[derive(Clone, Copy, PartialEq)]
pub enum AlignmentAxis {
Horizontal,
Vertical,
}
pub trait HorizAlignment: 'static {
fn id(&self) -> std::any::TypeId {
std::any::TypeId::of::<Self>()
}
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Mean
}
}
pub trait VertAlignment: 'static {
fn id(&self) -> std::any::TypeId {
std::any::TypeId::of::<Self>()
}
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Mean
}
}
pub struct Leading;
impl HorizAlignment for Leading {
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Min
}
}
/// Center alignment.
///
/// Note that this alignment can be used for both horizontal and vertical
/// alignment.
pub struct Center;
impl HorizAlignment for Center {}
impl VertAlignment for Center {}
pub struct Trailing;
impl HorizAlignment for Trailing {
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Max
}
}
pub struct Top;
impl VertAlignment for Top {
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Min
}
}
pub struct Bottom;
impl VertAlignment for Bottom {
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Max
}
}
pub struct FirstBaseline;
impl VertAlignment for FirstBaseline {
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Min
}
}
pub struct LastBaseline;
impl VertAlignment for LastBaseline {
fn merge(&self) -> AlignmentMerge {
AlignmentMerge::Max
}
}
#[derive(Clone, Copy)]
pub struct SingleAlignment {
id: std::any::TypeId,
merge: AlignmentMerge,
axis: AlignmentAxis,
}
impl SingleAlignment {
pub fn id(&self) -> std::any::TypeId {
self.id
}
pub fn axis(&self) -> AlignmentAxis {
self.axis
}
// Maybe these should all be dyn
pub fn from_horiz(h: &impl HorizAlignment) -> SingleAlignment {
SingleAlignment {
id: h.id(),
merge: h.merge(),
axis: AlignmentAxis::Horizontal,
}
}
pub fn from_dyn_horiz(h: &dyn HorizAlignment) -> SingleAlignment {
SingleAlignment {
id: h.id(),
merge: h.merge(),
axis: AlignmentAxis::Horizontal,
}
}
pub fn from_vert(v: &impl VertAlignment) -> SingleAlignment {
SingleAlignment {
id: v.id(),
merge: v.merge(),
axis: AlignmentAxis::Vertical,
}
}
pub fn from_dyn_vert(v: &dyn VertAlignment) -> SingleAlignment {
SingleAlignment {
id: v.id(),
merge: v.merge(),
axis: AlignmentAxis::Vertical,
}
}
pub fn apply_offset(&self, offset: Point, value: f64) -> f64 {
match self.axis {
AlignmentAxis::Horizontal => value + offset.x,
AlignmentAxis::Vertical => value + offset.y,
}
}
}
#[derive(Default)]
pub struct AlignResult {
value: f64,
count: usize,
}
impl AlignResult {
pub fn aggregate(&mut self, alignment: SingleAlignment, value: f64) {
match alignment.merge {
AlignmentMerge::Max => {
if self.count == 0 {
self.value = value;
} else {
self.value = self.value.max(value)
}
}
AlignmentMerge::Min => {
if self.count == 0 {
self.value = value;
} else {
self.value = self.value.min(value)
}
}
AlignmentMerge::Mean => self.value += value,
}
self.count += 1;
}
pub fn reap(&self, alignment: SingleAlignment) -> f64 {
match alignment.merge {
AlignmentMerge::Mean => {
if self.count == 0 {
0.0
} else {
self.value / self.count as f64
}
}
_ => self.value,
}
}
}
// AlignmentGuide widget
/// A proxy that can be queried for alignments.
pub struct AlignmentProxy<'a> {
widget_state: &'a WidgetState,
widget: &'a dyn AnyWidget,
}
struct AlignmentGuide<F> {
alignment_id: std::any::TypeId,
callback: F,
child: Box<dyn AnyWidget>,
}
impl<'a> AlignmentProxy<'a> {
pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 {
self.widget_state.get_alignment(self.widget, alignment)
}
pub fn get_horiz(&self, alignment: &dyn HorizAlignment) -> f64 {
self.get_alignment(SingleAlignment::from_dyn_horiz(alignment))
}
pub fn get_vert(&self, alignment: &dyn VertAlignment) -> f64 {
self.get_alignment(SingleAlignment::from_dyn_vert(alignment))
}
pub fn width(&self) -> f64 {
self.widget_state.size.width
}
pub fn height(&self) -> f64 {
self.widget_state.size.height
}
}
impl<F: Fn(AlignmentProxy) -> f64 + 'static> Widget for AlignmentGuide<F> {
fn event(&mut self, cx: &mut EventCx, event: &super::RawEvent) {
self.child.event(cx, event);
}
fn update(&mut self, cx: &mut super::UpdateCx) {
self.child.update(cx);
}
fn prelayout(
&mut self,
cx: &mut super::LayoutCx,
) -> (druid_shell::kurbo::Size, druid_shell::kurbo::Size) {
self.child.prelayout(cx)
}
fn layout(
&mut self,
cx: &mut super::LayoutCx,
proposed_size: druid_shell::kurbo::Size,
) -> druid_shell::kurbo::Size {
self.child.layout(cx, proposed_size)
}
fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {
if alignment.id == self.alignment_id {
let proxy = AlignmentProxy {
widget_state: cx.widget_state,
widget: self,
};
let value = (self.callback)(proxy);
cx.align_result.aggregate(alignment, value);
} else {
self.child.align(cx, alignment);
}
}
fn paint(&mut self, cx: &mut super::PaintCx) {
self.child.paint(cx);
}
}

75
src/widget/button.rs Normal file
View File

@ -0,0 +1,75 @@
// 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 druid_shell::{
kurbo::{Point, Size},
piet::{Color, RenderContext, Text, TextLayoutBuilder},
};
use crate::{event::Event, id::IdPath};
use super::{EventCx, LayoutCx, PaintCx, UpdateCx, Widget};
#[derive(Default)]
pub struct Button {
id_path: IdPath,
label: String,
}
impl Button {
pub fn new(id_path: &IdPath, label: String) -> Button {
Button {
id_path: id_path.clone(),
label,
}
}
pub fn set_label(&mut self, label: String) {
self.label = label;
}
}
const FIXED_SIZE: Size = Size::new(100., 20.);
impl Widget for Button {
fn update(&mut self, _cx: &mut UpdateCx) {
// TODO: probably want to request layout when string changes
}
fn event(&mut self, cx: &mut EventCx, _event: &super::RawEvent) {
cx.add_event(Event::new(self.id_path.clone(), ()));
}
fn prelayout(&mut self, _cx: &mut LayoutCx) -> (Size, Size) {
// TODO: do text layout here.
(FIXED_SIZE, FIXED_SIZE)
}
fn layout(&mut self, _cx: &mut LayoutCx, _proposed_size: Size) -> Size {
FIXED_SIZE
}
// TODO: alignment
fn paint(&mut self, ctx: &mut PaintCx) {
let layout = ctx
.text()
.new_text_layout(self.label.clone())
.text_color(Color::WHITE)
.build()
.unwrap();
ctx.draw_text(&layout, Point::ZERO);
}
}

159
src/widget/contexts.rs Normal file
View File

@ -0,0 +1,159 @@
// 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.
//! Contexts for the widget system.
//!
//! Note: the organization of this code roughly follows the existing Druid
//! widget system, particularly its contexts.rs.
use std::ops::{Deref, DerefMut};
use druid_shell::{
kurbo::Point,
piet::{Piet, PietText, RenderContext},
WindowHandle,
};
use crate::event::Event;
use super::{
align::{AlignResult, AlignmentAxis, SingleAlignment},
PodFlags, WidgetState,
};
// These contexts loosely follow Druid.
pub struct CxState<'a> {
window: &'a WindowHandle,
text: PietText,
events: &'a mut Vec<Event>,
}
pub struct EventCx<'a, 'b> {
pub(crate) cx_state: &'a mut CxState<'b>,
pub(crate) widget_state: &'a mut WidgetState,
}
pub struct UpdateCx<'a, 'b> {
pub(crate) cx_state: &'a mut CxState<'b>,
pub(crate) widget_state: &'a mut WidgetState,
}
pub struct LayoutCx<'a, 'b> {
pub(crate) cx_state: &'a mut CxState<'b>,
pub(crate) widget_state: &'a mut WidgetState,
}
pub struct AlignCx<'a> {
pub(crate) widget_state: &'a WidgetState,
pub(crate) align_result: &'a mut AlignResult,
pub(crate) origin: Point,
}
pub struct PaintCx<'a, 'b, 'c> {
pub(crate) cx_state: &'a mut CxState<'b>,
pub(crate) piet: &'a mut Piet<'c>,
}
impl<'a> CxState<'a> {
pub fn new(window: &'a WindowHandle, events: &'a mut Vec<Event>) -> Self {
CxState {
window,
text: window.text(),
events,
}
}
pub(crate) fn has_events(&self) -> bool {
!self.events.is_empty()
}
}
impl<'a, 'b> EventCx<'a, 'b> {
pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self {
EventCx {
cx_state,
widget_state: root_state,
}
}
pub fn add_event(&mut self, event: Event) {
self.cx_state.events.push(event);
}
}
impl<'a, 'b> UpdateCx<'a, 'b> {
pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self {
UpdateCx {
cx_state,
widget_state: root_state,
}
}
pub fn request_layout(&mut self) {
self.widget_state.flags |= PodFlags::REQUEST_LAYOUT;
}
}
impl<'a, 'b> LayoutCx<'a, 'b> {
pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self {
LayoutCx {
cx_state,
widget_state: root_state,
}
}
pub fn text(&mut self) -> &mut PietText {
&mut self.cx_state.text
}
pub fn add_event(&mut self, event: Event) {
self.cx_state.events.push(event);
}
}
impl<'a> AlignCx<'a> {
pub fn aggregate(&mut self, alignment: SingleAlignment, value: f64) {
let origin_value = match alignment.axis() {
AlignmentAxis::Horizontal => self.origin.x,
AlignmentAxis::Vertical => self.origin.y,
};
self.align_result.aggregate(alignment, value + origin_value);
}
}
impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> {
pub fn new(cx_state: &'a mut CxState<'b>, piet: &'a mut Piet<'c>) -> Self {
PaintCx { cx_state, piet }
}
pub fn with_save(&mut self, f: impl FnOnce(&mut PaintCx)) {
self.piet.save().unwrap();
f(self);
self.piet.restore().unwrap();
}
}
impl<'c> Deref for PaintCx<'_, '_, 'c> {
type Target = Piet<'c>;
fn deref(&self) -> &Self::Target {
self.piet
}
}
impl<'c> DerefMut for PaintCx<'_, '_, 'c> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.piet
}
}

205
src/widget/core.rs Normal file
View File

@ -0,0 +1,205 @@
// 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.
//! Core types and mechanisms for the widget hierarchy.
//!
//! //! Note: the organization of this code roughly follows the existing Druid
//! widget system, particularly its core.rs.
use bitflags::bitflags;
use druid_shell::{
kurbo::{Affine, Point, Size},
piet::RenderContext,
};
use crate::Widget;
use super::{
align::{
AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top,
Trailing, VertAlignment,
},
AlignCx, AnyWidget, EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx,
};
bitflags! {
#[derive(Default)]
pub(crate) struct PodFlags: u32 {
const REQUEST_UPDATE = 1;
const REQUEST_LAYOUT = 2;
const REQUEST_PAINT = 4;
const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits;
const INIT_FLAGS = Self::REQUEST_UPDATE.bits | Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits;
}
}
/// A pod that contains a widget (in a container).
pub struct Pod {
pub(crate) state: WidgetState,
pub(crate) widget: Box<dyn AnyWidget>,
}
#[derive(Default, Debug)]
pub(crate) struct WidgetState {
pub(crate) flags: PodFlags,
pub(crate) origin: Point,
/// The minimum intrinsic size of the widget.
pub(crate) min_size: Size,
/// The maximum intrinsic size of the widget.
pub(crate) max_size: Size,
/// The size proposed by the widget's container.
pub(crate) proposed_size: Size,
/// The size of the widget.
pub(crate) size: Size,
}
impl WidgetState {
fn merge_up(&mut self, child_state: &mut WidgetState) {
self.flags |= child_state.flags & PodFlags::UPWARD_FLAGS;
}
fn request(&mut self, flags: PodFlags) {
self.flags |= flags
}
/// Get alignment value.
///
/// The value is in the coordinate system of the parent widget.
pub(crate) fn get_alignment(&self, widget: &dyn AnyWidget, alignment: SingleAlignment) -> f64 {
if alignment.id() == Leading.id() || alignment.id() == Top.id() {
0.0
} else if alignment.id() == <Center as HorizAlignment>::id(&Center) {
match alignment.axis() {
AlignmentAxis::Horizontal => self.size.width * 0.5,
AlignmentAxis::Vertical => self.size.height * 0.5,
}
} else if alignment.id() == Trailing.id() {
self.size.width
} else if alignment.id() == Bottom.id() {
self.size.height
} else {
let mut align_result = AlignResult::default();
let mut align_cx = AlignCx {
widget_state: self,
align_result: &mut align_result,
origin: self.origin,
};
widget.align(&mut align_cx, alignment);
align_result.reap(alignment)
}
}
}
impl Pod {
pub fn new(widget: impl Widget + 'static) -> Self {
Self::new_from_box(Box::new(widget))
}
pub fn new_from_box(widget: Box<dyn AnyWidget>) -> Self {
Pod {
state: WidgetState {
flags: PodFlags::INIT_FLAGS,
..Default::default()
},
widget,
}
}
pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
(*self.widget).as_any_mut().downcast_mut()
}
pub fn request_update(&mut self) {
self.state.request(PodFlags::REQUEST_UPDATE);
}
pub fn event(&mut self, cx: &mut EventCx, event: &RawEvent) {
self.widget.event(cx, event);
}
/// Propagate an update cycle.
pub fn update(&mut self, cx: &mut UpdateCx) {
if self.state.flags.contains(PodFlags::REQUEST_UPDATE) {
let mut child_cx = UpdateCx {
cx_state: cx.cx_state,
widget_state: &mut self.state,
};
self.widget.update(&mut child_cx);
self.state.flags.remove(PodFlags::REQUEST_UPDATE);
cx.widget_state.merge_up(&mut self.state);
}
}
pub fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) {
let mut child_cx = LayoutCx {
cx_state: cx.cx_state,
widget_state: &mut self.state,
};
let (min_size, max_size) = self.widget.prelayout(&mut child_cx);
self.state.min_size = min_size;
self.state.max_size = max_size;
// Don't remove REQUEST_LAYOUT here, that will be done in layout.
}
(self.state.min_size, self.state.max_size)
}
pub fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size {
if self.state.flags.contains(PodFlags::REQUEST_LAYOUT)
|| proposed_size != self.state.proposed_size
{
let mut child_cx = LayoutCx {
cx_state: cx.cx_state,
widget_state: &mut self.state,
};
let new_size = self.widget.layout(&mut child_cx, proposed_size);
self.state.proposed_size = proposed_size;
self.state.size = new_size;
self.state.flags.remove(PodFlags::REQUEST_LAYOUT);
}
self.state.size
}
/// Propagate alignment query to children.
///
/// This call aggregates all instances of the alignment, so cost may be
/// proportional to the number of descendants.
pub fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {
let mut child_cx = AlignCx {
widget_state: &self.state,
align_result: cx.align_result,
origin: cx.origin + self.state.origin.to_vec2(),
};
self.widget.align(&mut child_cx, alignment);
}
pub fn paint(&mut self, cx: &mut PaintCx) {
cx.with_save(|cx| {
cx.piet
.transform(Affine::translate(self.state.origin.to_vec2()));
self.widget.paint(cx);
});
}
pub fn height_flexibility(&self) -> f64 {
self.state.max_size.height - self.state.min_size.height
}
/// The returned value is in the coordinate space of the parent that
/// owns this pod.
pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 {
self.state.get_alignment(&self.widget, alignment)
}
}

View File

@ -0,0 +1,96 @@
// 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.
//! The widget-side implementation of layout observers.
//!
//! This concept is very similar to GeometryReader in SwiftUI.
use druid_shell::kurbo::Size;
use crate::{event::Event, id::IdPath};
use super::{
align::SingleAlignment, AlignCx, AnyWidget, EventCx, LayoutCx, PaintCx, Pod, RawEvent,
UpdateCx, Widget,
};
pub struct LayoutObserver {
id_path: IdPath,
size: Option<Size>,
child: Option<Pod>,
}
impl LayoutObserver {
pub fn new(id_path: &IdPath) -> LayoutObserver {
LayoutObserver {
id_path: id_path.clone(),
size: None,
child: None,
}
}
pub fn set_child(&mut self, child: Box<dyn AnyWidget>) {
self.child = Some(Pod::new_from_box(child));
}
pub fn child_mut(&mut self) -> &mut Option<Pod> {
&mut self.child
}
}
impl Widget for LayoutObserver {
fn update(&mut self, cx: &mut UpdateCx) {
// Need to make sure we do layout on child when set.
cx.request_layout();
if let Some(child) = &mut self.child {
child.update(cx);
}
}
fn event(&mut self, cx: &mut EventCx, event: &RawEvent) {
if let Some(child) = &mut self.child {
child.event(cx, event);
}
}
fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
if let Some(child) = &mut self.child {
let _ = child.prelayout(cx);
}
(Size::ZERO, Size::new(1e9, 1e9))
}
fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size {
if Some(proposed_size) != self.size {
cx.add_event(Event::new(self.id_path.clone(), proposed_size));
self.size = Some(proposed_size);
}
if let Some(child) = &mut self.child {
let _ = child.layout(cx, proposed_size);
}
proposed_size
}
fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {
if let Some(child) = &self.child {
child.align(cx, alignment);
}
}
fn paint(&mut self, cx: &mut PaintCx) {
if let Some(child) = &mut self.child {
child.paint(cx);
}
}
}

106
src/widget/text.rs Normal file
View File

@ -0,0 +1,106 @@
// 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 druid_shell::{
kurbo::{Point, Size},
piet::{Color, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder},
};
use super::{
align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment},
AlignCx, EventCx, LayoutCx, PaintCx, UpdateCx, Widget,
};
pub struct TextWidget {
text: String,
color: Color,
layout: Option<PietTextLayout>,
is_wrapped: bool,
}
impl TextWidget {
pub fn new(text: String) -> TextWidget {
TextWidget {
text,
color: Color::WHITE,
layout: None,
is_wrapped: false,
}
}
pub fn set_text(&mut self, text: String) {
self.text = text;
self.layout = None;
}
}
impl Widget for TextWidget {
fn event(&mut self, _cx: &mut EventCx, _event: &super::RawEvent) {}
fn update(&mut self, cx: &mut UpdateCx) {
// All changes potentially require layout. Note: we could be finer
// grained, maybe color changes wouldn't.
cx.request_layout();
}
fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
let layout = cx
.text()
.new_text_layout(self.text.clone())
.text_color(self.color.clone())
.build()
.unwrap();
let min_size = Size::ZERO;
let max_size = layout.size();
self.layout = Some(layout);
self.is_wrapped = false;
(min_size, max_size)
}
fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size {
let needs_wrap = proposed_size.width < cx.widget_state.max_size.width;
if self.is_wrapped || needs_wrap {
let layout = cx
.text()
.new_text_layout(self.text.clone())
.max_width(proposed_size.width)
.text_color(self.color.clone())
.build()
.unwrap();
let size = layout.size();
self.layout = Some(layout);
self.is_wrapped = needs_wrap;
size
} else {
cx.widget_state.max_size
}
}
fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {
if alignment.id() == FirstBaseline.id() {
if let Some(metric) = self.layout.as_ref().unwrap().line_metric(0) {
cx.aggregate(alignment, metric.baseline);
}
} else if alignment.id() == LastBaseline.id() {
let i = self.layout.as_ref().unwrap().line_count() - 1;
if let Some(metric) = self.layout.as_ref().unwrap().line_metric(i) {
cx.aggregate(alignment, metric.y_offset + metric.baseline);
}
}
}
fn paint(&mut self, cx: &mut PaintCx) {
cx.draw_text(self.layout.as_ref().unwrap(), Point::ZERO);
}
}

135
src/widget/vstack.rs Normal file
View File

@ -0,0 +1,135 @@
// 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 druid_shell::kurbo::{Point, Rect, Size};
use super::{
align::{Center, SingleAlignment},
EventCx, LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget,
};
pub struct VStack {
children: Vec<Pod>,
alignment: SingleAlignment,
spacing: f64,
}
impl VStack {
pub fn new(children: Vec<Pod>) -> Self {
let alignment = SingleAlignment::from_horiz(&Center);
let spacing = 0.0;
VStack {
children,
alignment,
spacing,
}
}
pub fn children_mut(&mut self) -> &mut Vec<Pod> {
&mut self.children
}
}
impl Widget for VStack {
fn event(&mut self, cx: &mut EventCx, event: &super::RawEvent) {
match event {
RawEvent::MouseDown(p) => {
for child in &mut self.children {
let rect = Rect::from_origin_size(child.state.origin, child.state.size);
if rect.contains(*p) {
let child_event = RawEvent::MouseDown(*p - child.state.origin.to_vec2());
child.event(cx, &child_event);
break;
}
}
}
}
}
fn update(&mut self, cx: &mut UpdateCx) {
for child in &mut self.children {
child.update(cx);
}
}
fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
let mut min_size = Size::ZERO;
let mut max_size = Size::ZERO;
for child in &mut self.children {
let (child_min, child_max) = child.prelayout(cx);
min_size.width = min_size.width.max(child_min.width);
min_size.height += child_min.height;
max_size.width = max_size.width.max(child_max.width);
max_size.height += child_max.height;
}
let spacing = self.spacing * (self.children.len() - 1) as f64;
min_size.height += spacing;
max_size.height += spacing;
(min_size, max_size)
}
fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size {
// First, sort children in order of increasing flexibility
let mut child_order: Vec<_> = (0..self.children.len()).collect();
child_order.sort_by_key(|ix| self.children[*ix].height_flexibility().to_bits());
// Offer remaining height to each child
let mut n_remaining = self.children.len();
let mut height_remaining = proposed_size.height - (n_remaining - 1) as f64 * self.spacing;
for ix in child_order {
let child_height = (height_remaining / n_remaining as f64).max(0.0);
let child_proposed = Size::new(proposed_size.width, child_height);
let child_size = self.children[ix].layout(cx, child_proposed);
height_remaining -= child_size.height;
n_remaining -= 1;
}
// Get alignments from children
let alignments: Vec<f64> = self
.children
.iter()
.map(|child| child.get_alignment(self.alignment))
.collect();
let max_align = alignments
.iter()
.copied()
.reduce(f64::max)
.unwrap_or_default();
// Place children, using computed height and alignments
let mut size = Size::default();
let mut y = 0.0;
for (i, (child, align)) in self.children.iter_mut().zip(alignments).enumerate() {
if i != 0 {
y += self.spacing;
}
let child_size = child.state.size;
let origin = Point::new(max_align - align, y);
child.state.origin = origin;
size.width = size.width.max(child_size.width + origin.x);
y += child_size.height;
}
size.height = y;
size
}
fn align(&self, cx: &mut super::AlignCx, alignment: SingleAlignment) {
for child in &self.children {
child.align(cx, alignment);
}
}
fn paint(&mut self, cx: &mut PaintCx) {
for child in &mut self.children {
child.paint(cx);
}
}
}