Merge pull request #1 from linebender/new_infra

Work in progress xilem prototype
This commit is contained in:
Raph Levien 2022-11-23 13:24:11 -05:00 committed by GitHub
commit ef1d90732b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 3833 additions and 342 deletions

3050
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,32 @@
[package]
name = "xilem"
version = "0.1.0"
license = "Apache-2.0"
authors = ["Raph Levien <raph@google.com>"]
edition = "2021"
[dependencies]
"druid-shell" = "0.7"
bitflags = "1.3.2"
futures-task = "0.3"
tokio = { version = "1", features = ["full"] }
# This is just a bin for now but will be a lib + examples soon
[dev-dependencies]
sha2 = "0.10"
hex = "0.4.3"
[features]
default = ["x11"]
gtk = ["glazier/gtk"]
x11 = ["glazier/x11"]
wayland = ["glazier/wayland"]
[dependencies]
glazier = { git = "https://github.com/linebender/glazier", default-features = false }
piet-gpu-hal = { git = "https://github.com/linebender/piet-gpu" }
piet-gpu = { git = "https://github.com/linebender/piet-gpu" }
piet-scene = { git = "https://github.com/linebender/piet-gpu", features = ["kurbo"] }
raw-window-handle = "0.5"
png = "0.16.2"
rand = "0.7.3"
roxmltree = "0.13"
swash = "0.1.4"
bytemuck = { version = "1.7.2", features = ["derive"] }
parley = { git = "https://github.com/dfrg/parley" }
tokio = { version = "1.21", features = ["full"] }
futures-task = "0.3"
bitflags = "1.3.2"
[patch."https://github.com/dfrg/fount"]
fount = { git = "https://github.com/jneem/fount" }

View File

@ -16,14 +16,14 @@ use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use druid_shell::kurbo::Size;
use druid_shell::piet::{Color, Piet, RenderContext};
use druid_shell::{IdleHandle, IdleToken, WindowHandle};
use glazier::kurbo::Size;
use glazier::{IdleHandle, IdleToken, WindowHandle};
use parley::FontContext;
use tokio::runtime::Runtime;
use crate::event::{AsyncWake, EventResult};
use crate::id::IdPath;
use crate::widget::{CxState, EventCx, LayoutCx, PaintCx, Pod, UpdateCx, WidgetState};
use crate::widget::{CxState, EventCx, LayoutCx, PaintCx, Pod, Rendered, UpdateCx, WidgetState};
use crate::{
event::Event,
id::Id,
@ -42,6 +42,7 @@ pub struct App<T, V: View<T>> {
root_pod: Option<Pod>,
size: Size,
cx: Cx,
font_cx: FontContext,
pub(crate) rt: Runtime,
}
@ -92,8 +93,6 @@ enum UiState {
#[derive(Clone, Default)]
pub struct WakeQueue(Arc<Mutex<Vec<IdPath>>>);
const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22);
impl<T: Send + 'static, V: View<T> + 'static> App<T, V>
where
V::Element: Widget + 'static,
@ -153,6 +152,7 @@ where
root_state: Default::default(),
size: Default::default(),
cx,
font_cx: FontContext::new(),
rt,
}
}
@ -170,15 +170,14 @@ where
self.size = size;
}
pub fn paint(&mut self, piet: &mut Piet) {
let rect = self.size.to_rect();
piet.fill(rect, &BG_COLOR);
pub fn paint(&mut self) -> Rendered {
loop {
self.send_events();
// TODO: be more lazy re-rendering
self.render();
let root_pod = self.root_pod.as_mut().unwrap();
let mut cx_state = CxState::new(&self.window_handle, &mut self.events);
let mut cx_state =
CxState::new(&self.window_handle, &mut self.font_cx, &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);
@ -198,15 +197,15 @@ where
// Rerun app logic, primarily for virtualized scrolling
continue;
}
let mut paint_cx = PaintCx::new(&mut cx_state, &mut self.root_state, piet);
root_pod.paint(&mut paint_cx);
break;
let mut paint_cx = PaintCx::new(&mut cx_state, &mut self.root_state);
return root_pod.paint(&mut paint_cx);
}
}
pub fn window_event(&mut self, event: RawEvent) {
self.ensure_root();
let root_pod = self.root_pod.as_mut().unwrap();
let mut cx_state = CxState::new(&self.window_handle, &mut self.events);
let mut cx_state = CxState::new(&self.window_handle, &mut self.font_cx, &mut self.events);
let mut event_cx = EventCx::new(&mut cx_state, &mut self.root_state);
root_pod.event(&mut event_cx, &event);
self.send_events();
@ -219,6 +218,13 @@ where
}
}
// Make sure the widget tree (root pod) is available
fn ensure_root(&mut self) {
if self.root_pod.is_none() {
self.render();
}
}
/// Run the app logic and update the widget tree.
fn render(&mut self) {
if self.render_inner(false) {
@ -325,7 +331,7 @@ where
self.ui_state = UiState::Delayed;
}
}
}
},
Ok(None) => break,
Err(_) => {
self.render().await;

View File

@ -14,10 +14,12 @@
use std::any::Any;
use druid_shell::{
kurbo::Size, Application, Cursor, HotKey, IdleToken, Menu, MouseEvent, Region, SysMods,
WinHandler, WindowBuilder, WindowHandle,
use glazier::{
kurbo::Size, Application, Cursor, HotKey, IdleToken, Menu, MouseEvent, Region, Scalable,
SysMods, WinHandler, WindowBuilder, WindowHandle,
};
use parley::FontContext;
use piet_scene::{Scene, SceneBuilder, SceneFragment};
use crate::{app::App, widget::RawEvent, View, Widget};
@ -35,6 +37,10 @@ where
{
handle: WindowHandle,
app: App<T, V>,
pgpu_state: Option<crate::render::PgpuState>,
font_context: FontContext,
scene: Scene,
counter: u64,
}
const QUIT_MENU_ID: u32 = 0x100;
@ -58,7 +64,7 @@ impl<T: Send + 'static, V: View<T> + 'static> AppLauncher<T, V> {
QUIT_MENU_ID,
"E&xit",
Some(&HotKey::new(SysMods::Cmd, "q")),
true,
Some(true),
false,
);
let mut menubar = Menu::new();
@ -71,6 +77,7 @@ impl<T: Send + 'static, V: View<T> + 'static> AppLauncher<T, V> {
builder.set_handler(Box::new(main_state));
builder.set_title(self.title);
builder.set_menu(menubar);
builder.set_size(Size::new(1024., 768.));
let window = builder.build().unwrap();
window.show();
druid_app.run(None);
@ -88,8 +95,17 @@ where
fn prepare_paint(&mut self) {}
fn paint(&mut self, piet: &mut druid_shell::piet::Piet, _: &Region) {
self.app.paint(piet);
fn paint(&mut self, _: &Region) {
let rendered = self.app.paint();
self.render(rendered.0);
self.schedule_render();
}
// TODO: temporary hack
fn idle(&mut self, _: IdleToken) {
let rendered = self.app.paint();
self.render(rendered.0);
self.schedule_render();
}
fn command(&mut self, id: u32) {
@ -135,11 +151,6 @@ where
Application::global().quit()
}
fn idle(&mut self, _token: IdleToken) {
// TODO: wire up invalidation through widget hierarchy
self.handle.invalidate();
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
@ -153,7 +164,53 @@ where
let state = MainState {
handle: Default::default(),
app,
font_context: FontContext::new(),
pgpu_state: None,
scene: Scene::default(),
counter: 0,
};
state
}
#[cfg(target_os = "macos")]
fn schedule_render(&self) {
self.handle
.get_idle_handle()
.unwrap()
.schedule_idle(IdleToken::new(0));
}
#[cfg(not(target_os = "macos"))]
fn schedule_render(&self) {
self.handle.invalidate();
}
fn render(&mut self, fragment: SceneFragment) {
if self.pgpu_state.is_none() {
let handle = &self.handle;
let scale = handle.get_scale().unwrap();
let insets = handle.content_insets().to_px(scale);
let mut size = handle.get_size().to_px(scale);
size.width -= insets.x_value();
size.height -= insets.y_value();
println!("render size: {:?}", size);
self.pgpu_state = Some(
crate::render::PgpuState::new(
handle,
handle,
size.width as usize,
size.height as usize,
)
.unwrap(),
);
}
if let Some(pgpu_state) = self.pgpu_state.as_mut() {
if let Some(_timestamps) = pgpu_state.pre_render() {}
let mut builder = SceneBuilder::for_scene(&mut self.scene);
builder.append(&fragment, None);
//crate::test_scenes::render(&mut self.font_context, &mut self.scene, 0, self.counter);
self.counter += 1;
pgpu_state.render(&self.scene);
}
}
}

View File

@ -23,7 +23,7 @@ pub type IdPath = Vec<Id>;
impl Id {
/// Allocate a new, unique `Id`.
pub fn next() -> Id {
use druid_shell::Counter;
use glazier::Counter;
static WIDGET_ID_COUNTER: Counter = Counter::new();
Id(WIDGET_ID_COUNTER.next_nonzero())
}

View File

@ -1,40 +1,163 @@
// 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 render;
mod test_scenes;
mod text;
mod view;
mod view_seq;
mod widget;
pub use app::App;
pub use app_main::AppLauncher;
pub use view::adapt::Adapt;
pub use view::async_list::async_list;
pub use view::button::button;
pub use view::layout_observer::LayoutObserver;
pub use view::list::list;
pub use view::memoize::Memoize;
pub use view::scroll_view::scroll_view;
pub use view::vstack::v_stack;
pub use view::View;
pub use widget::align::{AlignmentAxis, AlignmentProxy, HorizAlignment, VertAlignment};
pub use widget::align::VertAlignment;
pub use widget::Widget;
use glazier::kurbo::Size;
use glazier::{
Application, Cursor, FileDialogToken, FileInfo, IdleToken, KeyEvent, MouseEvent, Region,
Scalable, TimerToken, WinHandler, WindowHandle,
};
use parley::FontContext;
use piet_scene::Scene;
use std::any::Any;
pub struct WindowState {
handle: WindowHandle,
pgpu_state: Option<render::PgpuState>,
scene: Scene,
font_context: FontContext,
counter: u64,
}
impl WindowState {
pub fn new() -> Self {
Self {
handle: Default::default(),
pgpu_state: None,
scene: Default::default(),
font_context: FontContext::new(),
counter: 0,
}
}
#[cfg(target_os = "macos")]
fn schedule_render(&self) {
self.handle
.get_idle_handle()
.unwrap()
.schedule_idle(IdleToken::new(0));
}
#[cfg(not(target_os = "macos"))]
fn schedule_render(&self) {
self.handle.invalidate();
}
fn render(&mut self) {
if self.pgpu_state.is_none() {
let handle = &self.handle;
let scale = handle.get_scale().unwrap();
let insets = handle.content_insets().to_px(scale);
let mut size = handle.get_size().to_px(scale);
size.width -= insets.x_value();
size.height -= insets.y_value();
println!("render size: {:?}", size);
self.pgpu_state = Some(
render::PgpuState::new(handle, handle, size.width as usize, size.height as usize)
.unwrap(),
);
}
if let Some(pgpu_state) = self.pgpu_state.as_mut() {
if let Some(_timestamps) = pgpu_state.pre_render() {}
test_scenes::render(&mut self.font_context, &mut self.scene, 0, self.counter);
self.counter += 1;
pgpu_state.render(&self.scene);
}
}
}
impl WinHandler for WindowState {
fn connect(&mut self, handle: &WindowHandle) {
self.handle = handle.clone();
self.schedule_render();
}
fn prepare_paint(&mut self) {}
fn paint(&mut self, _: &Region) {
self.render();
self.schedule_render();
}
fn idle(&mut self, _: IdleToken) {
self.render();
self.schedule_render();
}
fn command(&mut self, _id: u32) {}
fn open_file(&mut self, _token: FileDialogToken, file_info: Option<FileInfo>) {
println!("open file result: {:?}", file_info);
}
fn save_as(&mut self, _token: FileDialogToken, file: Option<FileInfo>) {
println!("save file result: {:?}", file);
}
fn key_down(&mut self, event: KeyEvent) -> bool {
println!("keydown: {:?}", event);
false
}
fn key_up(&mut self, event: KeyEvent) {
println!("keyup: {:?}", event);
}
fn wheel(&mut self, event: &MouseEvent) {
println!("mouse_wheel {:?}", event);
}
fn mouse_move(&mut self, _event: &MouseEvent) {
self.handle.set_cursor(&Cursor::Arrow);
//println!("mouse_move {:?}", event);
}
fn mouse_down(&mut self, event: &MouseEvent) {
println!("mouse_down {:?}", event);
}
fn mouse_up(&mut self, event: &MouseEvent) {
println!("mouse_up {:?}", event);
}
fn timer(&mut self, id: TimerToken) {
println!("timer fired: {:?}", id);
}
fn size(&mut self, _size: Size) {
//self.size = size;
}
fn got_focus(&mut self) {
println!("Got focus");
}
fn lost_focus(&mut self) {
println!("Lost focus");
}
fn request_close(&mut self) {
self.handle.close();
}
fn destroy(&mut self) {
Application::global().quit()
}
fn as_any(&mut self) -> &mut dyn Any {
self
}
}

20
src/main.rs Normal file
View File

@ -0,0 +1,20 @@
use xilem::{button, App, AppLauncher, View};
fn app_logic(_data: &mut ()) -> impl View<()> {
button("click me", |_| println!("clicked"))
}
fn main() {
/*
let app = Application::new().unwrap();
let mut window_builder = glazier::WindowBuilder::new(app.clone());
window_builder.resizable(false);
window_builder.set_size((WIDTH as f64 / 2., HEIGHT as f64 / 2.).into());
window_builder.set_handler(Box::new(xilem::WindowState::new()));
let window_handle = window_builder.build().unwrap();
window_handle.show();
app.run(None);
*/
let app = App::new((), app_logic);
AppLauncher::new(app).run()
}

123
src/render.rs Normal file
View File

@ -0,0 +1,123 @@
use piet_gpu::Renderer;
use piet_gpu_hal::{
CmdBuf, Error, ImageLayout, Instance, QueryPool, Semaphore, Session, SubmittedCmdBuf, Surface,
Swapchain,
};
use piet_scene::Scene;
pub const NUM_FRAMES: usize = 2;
pub struct PgpuState {
#[allow(unused)]
instance: Instance,
#[allow(unused)]
surface: Option<Surface>,
swapchain: Swapchain,
session: Session,
present_semaphores: Vec<Semaphore>,
query_pools: Vec<QueryPool>,
cmd_bufs: [Option<CmdBuf>; NUM_FRAMES],
submitted: [Option<SubmittedCmdBuf>; NUM_FRAMES],
renderer: Renderer,
current_frame: usize,
}
impl PgpuState {
pub fn new(
window: &dyn raw_window_handle::HasRawWindowHandle,
display: &dyn raw_window_handle::HasRawDisplayHandle,
width: usize,
height: usize,
) -> Result<Self, Error> {
println!("size: {}, {}", width, height);
let instance = Instance::new(Default::default())?;
let surface = unsafe {
instance
.surface(display.raw_display_handle(), window.raw_window_handle())
.ok()
};
unsafe {
let device = instance.device()?;
let swapchain =
instance.swapchain(width, height, &device, surface.as_ref().unwrap())?;
let session = Session::new(device);
let present_semaphores = (0..NUM_FRAMES)
.map(|_| session.create_semaphore())
.collect::<Result<Vec<_>, Error>>()?;
let query_pools = (0..NUM_FRAMES)
.map(|_| session.create_query_pool(Renderer::QUERY_POOL_SIZE))
.collect::<Result<Vec<_>, Error>>()?;
let cmd_bufs: [Option<CmdBuf>; NUM_FRAMES] = Default::default();
let submitted: [Option<SubmittedCmdBuf>; NUM_FRAMES] = Default::default();
let renderer = Renderer::new(&session, width, height, NUM_FRAMES)?;
let current_frame = 0;
Ok(Self {
instance,
surface,
swapchain,
session,
present_semaphores,
query_pools,
cmd_bufs,
submitted,
renderer,
current_frame,
})
}
}
pub fn frame_index(&self) -> usize {
self.current_frame % NUM_FRAMES
}
pub fn pre_render(&mut self) -> Option<Vec<f64>> {
let frame_idx = self.frame_index();
if let Some(submitted) = self.submitted[frame_idx].take() {
self.cmd_bufs[frame_idx] = submitted.wait().unwrap();
Some(unsafe {
self.session
.fetch_query_pool(&self.query_pools[frame_idx])
.unwrap()
})
} else {
None
}
}
pub fn render(&mut self, scene: &Scene) {
let frame_idx = self.frame_index();
self.renderer.upload_scene(&scene, frame_idx).unwrap();
unsafe {
let (image_idx, acquisition_semaphore) = self.swapchain.next().unwrap();
let swap_image = self.swapchain.image(image_idx);
let query_pool = &self.query_pools[frame_idx];
let mut cmd_buf = self.cmd_bufs[frame_idx]
.take()
.unwrap_or_else(|| self.session.cmd_buf().unwrap());
cmd_buf.begin();
self.renderer.record(&mut cmd_buf, &query_pool, frame_idx);
// Image -> Swapchain
cmd_buf.image_barrier(&swap_image, ImageLayout::Undefined, ImageLayout::BlitDst);
cmd_buf.blit_image(&self.renderer.image_dev, &swap_image);
cmd_buf.image_barrier(&swap_image, ImageLayout::BlitDst, ImageLayout::Present);
cmd_buf.finish();
self.submitted[frame_idx] = Some(
self.session
.run_cmd_buf(
cmd_buf,
&[&acquisition_semaphore],
&[&self.present_semaphores[frame_idx]],
)
.unwrap(),
);
self.swapchain
.present(image_idx, &[&self.present_semaphores[frame_idx]])
.unwrap();
}
self.current_frame += 1;
}
}

62
src/test_scenes.rs Normal file
View File

@ -0,0 +1,62 @@
use super::text::*;
use parley::FontContext;
use piet_scene::*;
pub fn render(fcx: &mut FontContext, scene: &mut Scene, which: usize, arg: u64) {
match which {
_ => basic_scene(fcx, scene, arg),
}
}
fn basic_scene(fcx: &mut FontContext, scene: &mut Scene, arg: u64) {
let transform = Affine::translate(400.0, 400.0) * Affine::rotate((arg as f64 * 0.01) as f32);
let mut builder = SceneBuilder::for_scene(scene);
let stops = &[
GradientStop {
offset: 0.0,
color: Color::rgb8(128, 0, 0),
},
GradientStop {
offset: 0.5,
color: Color::rgb8(0, 128, 0),
},
GradientStop {
offset: 1.0,
color: Color::rgb8(0, 0, 128),
},
][..];
let gradient = Brush::LinearGradient(LinearGradient {
start: Point::new(0.0, 0.0),
end: Point::new(0.0, 400.0),
extend: ExtendMode::Pad,
stops: stops.iter().copied().collect(),
});
builder.fill(
Fill::NonZero,
transform,
&gradient,
None,
Rect {
min: Point::new(0.0, 0.0),
max: Point::new(600.0, 400.0),
}
.elements(),
);
let scale = (arg as f64 * 0.01).sin() * 0.5 + 1.5;
let mut lcx = parley::LayoutContext::new();
let mut layout_builder =
lcx.ranged_builder(fcx, "Hello piet-gpu! ഹലോ ਸਤ ਸ੍ਰੀ ਅਕਾਲ مرحبا!", scale as f32);
layout_builder.push_default(&parley::style::StyleProperty::FontSize(34.0));
layout_builder.push(
&parley::style::StyleProperty::Brush(ParleyBrush(Brush::Solid(Color::rgb8(255, 255, 0)))),
6..10,
);
layout_builder.push(&parley::style::StyleProperty::FontSize(48.0), 6..10);
layout_builder.push_default(&parley::style::StyleProperty::Brush(ParleyBrush(
Brush::Solid(Color::rgb8(255, 255, 255)),
)));
let mut layout = layout_builder.build();
layout.break_all_lines(None, parley::layout::Alignment::Start);
render_text(&mut builder, Affine::translate(100.0, 400.0), &layout);
builder.finish();
}

54
src/text.rs Normal file
View File

@ -0,0 +1,54 @@
use parley::Layout;
use piet_scene::{
glyph::{
pinot::{types::Tag, FontRef},
GlyphContext,
},
*,
};
#[derive(Clone, Debug)]
pub struct ParleyBrush(pub Brush);
impl Default for ParleyBrush {
fn default() -> ParleyBrush {
ParleyBrush(Brush::Solid(Color::rgb8(0, 0, 0)))
}
}
impl PartialEq<ParleyBrush> for ParleyBrush {
fn eq(&self, _other: &ParleyBrush) -> bool {
true // FIXME
}
}
impl parley::style::Brush for ParleyBrush {}
pub fn render_text(builder: &mut SceneBuilder, transform: Affine, layout: &Layout<ParleyBrush>) {
let mut gcx = GlyphContext::new();
for line in layout.lines() {
for glyph_run in line.glyph_runs() {
let mut x = glyph_run.offset();
let y = glyph_run.baseline();
let run = glyph_run.run();
let font = run.font().as_ref();
let font_size = run.font_size();
let font_ref = FontRef {
data: font.data,
offset: font.offset,
};
let style = glyph_run.style();
let vars: [(Tag, f32); 0] = [];
let mut gp = gcx.new_provider(&font_ref, None, font_size, false, vars);
for glyph in glyph_run.glyphs() {
if let Some(fragment) = gp.get(glyph.id, Some(&style.brush.0)) {
let gx = x + glyph.x;
let gy = y - glyph.y;
let xform = Affine::translate(gx, gy) * Affine::scale(1.0, -1.0);
builder.append(&fragment, Some(transform * xform));
}
x += glyph.advance;
}
}
}
}

View File

@ -12,17 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod adapt;
pub mod any_view;
pub mod async_list;
// pub mod adapt;
// pub mod any_view;
// pub mod async_list;
pub mod button;
pub mod layout_observer;
pub mod list;
pub mod memoize;
pub mod scroll_view;
// pub mod layout_observer;
// pub mod list;
// pub mod memoize;
// pub mod scroll_view;
pub mod text;
pub mod use_state;
pub mod vstack;
// pub mod use_state;
// pub mod vstack;
use std::{
any::Any,

View File

@ -1,122 +0,0 @@
// 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>: Send {
type State: Send;
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
);

View File

@ -16,17 +16,18 @@ pub mod align;
pub mod button;
mod contexts;
mod core;
pub mod layout_observer;
pub mod list;
//pub mod layout_observer;
//pub mod list;
pub mod piet_scene_helpers;
mod raw_event;
pub mod scroll_view;
//pub mod scroll_view;
pub mod text;
pub mod vstack;
//pub mod vstack;
use std::any::Any;
use std::ops::{Deref, DerefMut};
use druid_shell::kurbo::{Rect, Size};
use glazier::kurbo::{Rect, Size};
use self::contexts::LifeCycleCx;
pub use self::contexts::{AlignCx, CxState, EventCx, LayoutCx, PaintCx, PreparePaintCx, UpdateCx};
@ -78,9 +79,11 @@ pub trait Widget {
#[allow(unused)]
fn prepare_paint(&mut self, cx: &mut LayoutCx, visible: Rect) {}
fn paint(&mut self, cx: &mut PaintCx);
fn paint(&mut self, cx: &mut PaintCx) -> Rendered;
}
pub struct Rendered(pub(crate) piet_scene::SceneFragment);
pub trait AnyWidget: Widget {
fn as_any(&self) -> &dyn Any;
@ -132,8 +135,8 @@ impl Widget for Box<dyn AnyWidget> {
self.deref_mut().prepare_paint(cx, visible)
}
fn paint(&mut self, cx: &mut PaintCx) {
self.deref_mut().paint(cx);
fn paint(&mut self, cx: &mut PaintCx) -> Rendered {
self.deref_mut().paint(cx)
}
}

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use druid_shell::kurbo::Point;
use glazier::kurbo::{Point, Size};
use super::{contexts::LifeCycleCx, AlignCx, AnyWidget, EventCx, LifeCycle, Widget, WidgetState};
use super::{
contexts::LifeCycleCx, AlignCx, AnyWidget, EventCx, LifeCycle, Rendered, Widget, WidgetState,
};
#[derive(Clone, Copy, PartialEq)]
pub enum AlignmentMerge {
@ -253,18 +255,11 @@ impl<F: Fn(AlignmentProxy) -> f64 + 'static> Widget for AlignmentGuide<F> {
self.child.update(cx);
}
fn measure(
&mut self,
cx: &mut super::LayoutCx,
) -> (druid_shell::kurbo::Size, druid_shell::kurbo::Size) {
fn measure(&mut self, cx: &mut super::LayoutCx) -> (Size, Size) {
self.child.measure(cx)
}
fn layout(
&mut self,
cx: &mut super::LayoutCx,
proposed_size: druid_shell::kurbo::Size,
) -> druid_shell::kurbo::Size {
fn layout(&mut self, cx: &mut super::LayoutCx, proposed_size: Size) -> Size {
self.child.layout(cx, proposed_size)
}
@ -281,7 +276,7 @@ impl<F: Fn(AlignmentProxy) -> f64 + 'static> Widget for AlignmentGuide<F> {
}
}
fn paint(&mut self, cx: &mut super::PaintCx) {
self.child.paint(cx);
fn paint(&mut self, cx: &mut super::PaintCx) -> Rendered {
self.child.paint(cx)
}
}

View File

@ -12,26 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use druid_shell::{
kurbo::{Insets, Size},
piet::{
Color, LinearGradient, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder,
UnitPoint,
},
};
use glazier::kurbo::{Insets, Size};
use parley::Layout;
use piet_scene::{Affine, Brush, Color, GradientStop, GradientStops, SceneBuilder, SceneFragment};
use crate::{event::Event, id::IdPath, VertAlignment};
use crate::{event::Event, id::IdPath, text::ParleyBrush, VertAlignment};
use super::{
align::{FirstBaseline, LastBaseline, SingleAlignment},
contexts::LifeCycleCx,
AlignCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget,
piet_scene_helpers::{self, UnitPoint},
AlignCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, Rendered, UpdateCx, Widget,
};
pub struct Button {
id_path: IdPath,
label: String,
layout: Option<PietTextLayout>,
layout: Option<Layout<ParleyBrush>>,
}
impl Button {
@ -84,15 +81,18 @@ impl Widget for Button {
fn measure(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
let padding = Size::new(LABEL_INSETS.x_value(), LABEL_INSETS.y_value());
let min_height = 24.0;
let layout = cx
.text()
.new_text_layout(self.label.clone())
.text_color(Color::rgb8(0xf0, 0xf0, 0xea))
.build()
.unwrap();
let mut lcx = parley::LayoutContext::new();
let mut layout_builder = lcx.ranged_builder(cx.font_cx(), &self.label, 1.0);
layout_builder.push_default(&parley::style::StyleProperty::Brush(ParleyBrush(
Brush::Solid(Color::rgb8(0xf0, 0xf0, 0xea)),
)));
let mut layout = layout_builder.build();
// Question for Chad: is this needed?
layout.break_all_lines(None, parley::layout::Alignment::Start);
let size = Size::new(
layout.size().width + padding.width,
(layout.size().height + padding.height).max(min_height),
layout.width() as f64 + padding.width,
(layout.height() as f64 + padding.height).max(min_height),
);
self.layout = Some(layout);
(Size::new(10.0, min_height), size)
@ -105,10 +105,13 @@ impl Widget for Button {
.clamp(cx.min_size().width, cx.max_size().width),
cx.max_size().height,
);
println!("size = {:?}", size);
size
}
fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {
// TODO: figure this out
/*
if alignment.id() == FirstBaseline.id() || alignment.id() == LastBaseline.id() {
let layout = self.layout.as_ref().unwrap();
if let Some(metric) = layout.line_metric(0) {
@ -116,9 +119,10 @@ impl Widget for Button {
cx.aggregate(alignment, value);
}
}
*/
}
fn paint(&mut self, cx: &mut PaintCx) {
fn paint(&mut self, cx: &mut PaintCx) -> Rendered {
let is_hot = cx.is_hot();
let is_active = cx.is_active();
let button_border_width = 2.0;
@ -132,6 +136,32 @@ impl Widget for Button {
} else {
Color::rgb8(0x3a, 0x3a, 0x3a)
};
let bg_stops = if is_active {
[
GradientStop {
offset: 0.0,
color: Color::rgb8(0x3a, 0x3a, 0x3a),
},
GradientStop {
offset: 1.0,
color: Color::rgb8(0xa1, 0xa1, 0xa1),
},
][..]
.into()
} else {
[
GradientStop {
offset: 0.0,
color: Color::rgb8(0xa1, 0xa1, 0xa1),
},
GradientStop {
offset: 1.0,
color: Color::rgb8(0x3a, 0x3a, 0x3a),
},
][..]
.into()
};
/*
let bg_gradient = if is_active {
LinearGradient::new(
UnitPoint::TOP,
@ -145,10 +175,29 @@ impl Widget for Button {
(Color::rgb8(0xa1, 0xa1, 0xa1), Color::rgb8(0x3a, 0x3a, 0x3a)),
)
};
cx.stroke(rounded_rect, &border_color, button_border_width);
cx.fill(rounded_rect, &bg_gradient);
let layout = self.layout.as_ref().unwrap();
let offset = (cx.size().to_vec2() - layout.size().to_vec2()) * 0.5;
cx.draw_text(layout, offset.to_point());
*/
let mut fragment = SceneFragment::default();
let mut builder = SceneBuilder::for_fragment(&mut fragment);
piet_scene_helpers::stroke(
&mut builder,
&rounded_rect,
&Brush::Solid(border_color),
button_border_width,
);
piet_scene_helpers::fill_lin_gradient(
&mut builder,
&rounded_rect,
bg_stops,
UnitPoint::TOP,
UnitPoint::BOTTOM,
);
//cx.fill(rounded_rect, &bg_gradient);
if let Some(layout) = &self.layout {
let size = Size::new(layout.width() as f64, layout.height() as f64);
let offset = (cx.size().to_vec2() - size.to_vec2()) * 0.5;
let transform = Affine::translate(offset.x as f32, offset.y as f32);
crate::text::render_text(&mut builder, transform, &layout);
}
Rendered(fragment)
}
}

View File

@ -19,11 +19,11 @@
use std::ops::{Deref, DerefMut};
use druid_shell::{
use glazier::{
kurbo::{Point, Size},
piet::{Piet, PietText, RenderContext},
WindowHandle,
};
use parley::FontContext;
use crate::event::Event;
@ -35,7 +35,7 @@ use super::{
// These contexts loosely follow Druid.
pub struct CxState<'a> {
window: &'a WindowHandle,
text: PietText,
font_cx: &'a mut FontContext,
events: &'a mut Vec<Event>,
}
@ -66,17 +66,20 @@ pub struct AlignCx<'a> {
pub(crate) origin: Point,
}
pub struct PaintCx<'a, 'b, 'c> {
pub struct PaintCx<'a, 'b> {
pub(crate) cx_state: &'a mut CxState<'b>,
pub(crate) widget_state: &'a WidgetState,
pub(crate) piet: &'a mut Piet<'c>,
}
impl<'a> CxState<'a> {
pub fn new(window: &'a WindowHandle, events: &'a mut Vec<Event>) -> Self {
pub fn new(
window: &'a WindowHandle,
font_cx: &'a mut FontContext,
events: &'a mut Vec<Event>,
) -> Self {
CxState {
window,
text: window.text(),
font_cx,
events,
}
}
@ -143,10 +146,6 @@ impl<'a, 'b> LayoutCx<'a, 'b> {
}
}
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);
}
@ -161,6 +160,10 @@ impl<'a, 'b> LayoutCx<'a, 'b> {
pub fn max_size(&self) -> Size {
self.widget_state.max_size
}
pub fn font_cx(&mut self) -> &mut FontContext {
self.cx_state.font_cx
}
}
// This is laziness, should be a separate cx with invalidate methods
@ -180,25 +183,14 @@ impl<'a> AlignCx<'a> {
}
}
impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> {
pub(crate) fn new(
cx_state: &'a mut CxState<'b>,
widget_state: &'a mut WidgetState,
piet: &'a mut Piet<'c>,
) -> Self {
impl<'a, 'b> PaintCx<'a, 'b> {
pub(crate) fn new(cx_state: &'a mut CxState<'b>, widget_state: &'a mut WidgetState) -> Self {
PaintCx {
cx_state,
widget_state,
piet,
}
}
pub fn with_save(&mut self, f: impl FnOnce(&mut PaintCx)) {
self.piet.save().unwrap();
f(self);
self.piet.restore().unwrap();
}
pub fn is_hot(&self) -> bool {
self.widget_state.flags.contains(PodFlags::IS_HOT)
}
@ -210,18 +202,8 @@ impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> {
pub fn size(&self) -> Size {
self.widget_state.size
}
}
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
pub fn font_cx(&mut self) -> &mut FontContext {
self.cx_state.font_cx
}
}

View File

@ -18,10 +18,7 @@
//! widget system, particularly its core.rs.
use bitflags::bitflags;
use druid_shell::{
kurbo::{Affine, Point, Rect, Size},
piet::RenderContext,
};
use glazier::kurbo::{Affine, Point, Rect, Size};
use crate::Widget;
@ -32,7 +29,7 @@ use super::{
},
contexts::LifeCycleCx,
AlignCx, AnyWidget, CxState, EventCx, LayoutCx, LifeCycle, PaintCx, PreparePaintCx, RawEvent,
UpdateCx,
Rendered, UpdateCx,
};
bitflags! {
@ -277,6 +274,7 @@ impl Pod {
widget_state: &mut self.state,
};
let new_size = self.widget.layout(&mut child_cx, proposed_size);
println!("layout size = {:?}", new_size);
self.state.proposed_size = proposed_size;
self.state.size = new_size;
self.state.flags.remove(PodFlags::REQUEST_LAYOUT);
@ -301,7 +299,6 @@ impl Pod {
let mut inner_cx = PaintCx {
cx_state: cx.cx_state,
widget_state: &mut self.state,
piet: cx.piet,
};
self.widget.paint(&mut inner_cx);
}
@ -310,12 +307,12 @@ impl Pod {
self.widget.prepare_paint(cx, visible);
}
pub fn paint(&mut self, cx: &mut PaintCx) {
cx.with_save(|cx| {
cx.piet
.transform(Affine::translate(self.state.origin.to_vec2()));
self.paint_raw(cx);
});
pub fn paint(&mut self, cx: &mut PaintCx) -> Rendered {
let mut inner_cx = PaintCx {
cx_state: cx.cx_state,
widget_state: &mut self.state,
};
self.widget.paint(&mut inner_cx)
}
pub fn height_flexibility(&self) -> f64 {

View File

@ -0,0 +1,96 @@
use glazier::kurbo::{self, Rect, Shape};
use piet_scene::{
Affine, Brush, Cap, ExtendMode, Fill, GradientStops, Join, LinearGradient, PathElement, Point,
SceneBuilder, Stroke,
};
#[derive(Debug, Clone, Copy)]
pub struct UnitPoint {
u: f64,
v: f64,
}
pub fn stroke(builder: &mut SceneBuilder, path: &impl Shape, brush: &Brush, stroke_width: f64) {
let style = Stroke {
width: stroke_width as f32,
join: Join::Round,
miter_limit: 1.0,
start_cap: Cap::Round,
end_cap: Cap::Round,
dash_pattern: [],
dash_offset: 0.0,
scale: false,
};
// TODO: figure out how to avoid allocation
// (Just removing the collect should work in theory, but running into a clone bound)
let elements = path
.path_elements(1e-3)
.map(PathElement::from_kurbo)
.collect::<Vec<_>>();
builder.stroke(&style, Affine::IDENTITY, brush, None, &elements)
}
// Note: copied from piet
impl UnitPoint {
/// `(0.0, 0.0)`
pub const TOP_LEFT: UnitPoint = UnitPoint::new(0.0, 0.0);
/// `(0.5, 0.0)`
pub const TOP: UnitPoint = UnitPoint::new(0.5, 0.0);
/// `(1.0, 0.0)`
pub const TOP_RIGHT: UnitPoint = UnitPoint::new(1.0, 0.0);
/// `(0.0, 0.5)`
pub const LEFT: UnitPoint = UnitPoint::new(0.0, 0.5);
/// `(0.5, 0.5)`
pub const CENTER: UnitPoint = UnitPoint::new(0.5, 0.5);
/// `(1.0, 0.5)`
pub const RIGHT: UnitPoint = UnitPoint::new(1.0, 0.5);
/// `(0.0, 1.0)`
pub const BOTTOM_LEFT: UnitPoint = UnitPoint::new(0.0, 1.0);
/// `(0.5, 1.0)`
pub const BOTTOM: UnitPoint = UnitPoint::new(0.5, 1.0);
/// `(1.0, 1.0)`
pub const BOTTOM_RIGHT: UnitPoint = UnitPoint::new(1.0, 1.0);
/// Create a new UnitPoint.
///
/// The `u` and `v` coordinates describe the point, with (0.0, 0.0) being
/// the top-left, and (1.0, 1.0) being the bottom-right.
pub const fn new(u: f64, v: f64) -> UnitPoint {
UnitPoint { u, v }
}
/// Given a rectangle, resolve the point within the rectangle.
pub fn resolve(self, rect: Rect) -> kurbo::Point {
kurbo::Point::new(
rect.x0 + self.u * (rect.x1 - rect.x0),
rect.y0 + self.v * (rect.y1 - rect.y0),
)
}
}
pub fn fill_lin_gradient(
builder: &mut SceneBuilder,
path: &impl Shape,
stops: GradientStops,
start: UnitPoint,
end: UnitPoint,
) {
let rect = path.bounding_box();
let lin_grad = LinearGradient {
start: Point::from_kurbo(start.resolve(rect)),
end: Point::from_kurbo(end.resolve(rect)),
stops,
extend: ExtendMode::Pad,
};
let elements = path
.path_elements(1e-3)
.map(PathElement::from_kurbo)
.collect::<Vec<_>>();
builder.fill(
Fill::NonZero,
Affine::IDENTITY,
&Brush::LinearGradient(lin_grad),
None,
elements,
);
}

View File

@ -1,4 +1,4 @@
use druid_shell::{
use glazier::{
kurbo::{Point, Vec2},
Modifiers, MouseButton, MouseButtons,
};
@ -44,9 +44,9 @@ pub enum LifeCycle {
HotChanged(bool),
}
impl<'a> From<&'a druid_shell::MouseEvent> for MouseEvent {
fn from(src: &druid_shell::MouseEvent) -> MouseEvent {
let druid_shell::MouseEvent {
impl<'a> From<&'a glazier::MouseEvent> for MouseEvent {
fn from(src: &glazier::MouseEvent) -> MouseEvent {
let glazier::MouseEvent {
pos,
buttons,
mods,

View File

@ -12,21 +12,21 @@
// 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 glazier::kurbo::{Point, Size};
use parley::Layout;
use piet_scene::{Affine, Brush, Color, SceneBuilder, SceneFragment};
use crate::text::ParleyBrush;
use super::{
align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment},
contexts::LifeCycleCx,
AlignCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget,
AlignCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, Rendered, UpdateCx, Widget,
};
pub struct TextWidget {
text: String,
color: Color,
layout: Option<PietTextLayout>,
layout: Option<Layout<ParleyBrush>>,
is_wrapped: bool,
}
@ -34,15 +34,13 @@ impl TextWidget {
pub fn new(text: String) -> TextWidget {
TextWidget {
text,
color: Color::WHITE,
layout: None,
is_wrapped: false,
layout: None,
}
}
pub fn set_text(&mut self, text: String) {
self.text = text;
self.layout = None;
}
}
@ -58,52 +56,34 @@ impl Widget for TextWidget {
}
fn measure(&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);
let max_size = Size::new(50.0, 50.0);
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
}
let mut lcx = parley::LayoutContext::new();
let mut layout_builder = lcx.ranged_builder(cx.font_cx(), &self.text, 1.0);
layout_builder.push_default(&parley::style::StyleProperty::Brush(ParleyBrush(
Brush::Solid(Color::rgb8(255, 255, 255)),
)));
let mut layout = layout_builder.build();
// Question for Chad: is this needed?
layout.break_all_lines(None, parley::layout::Alignment::Start);
self.layout = Some(layout);
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 align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {}
fn paint(&mut self, cx: &mut PaintCx) {
cx.draw_text(self.layout.as_ref().unwrap(), Point::ZERO);
fn paint(&mut self, cx: &mut PaintCx) -> Rendered {
let mut fragment = SceneFragment::default();
let mut builder = SceneBuilder::for_fragment(&mut fragment);
if let Some(layout) = &self.layout {
let transform = Affine::translate(40.0, 40.0);
crate::text::render_text(&mut builder, transform, &layout);
}
Rendered(fragment)
}
}