diff --git a/Cargo.toml b/Cargo.toml index 74fba2bd0..2676603e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ conv = "0.3" log = "0.4" rusttype = "0.8" smallvec = "1.4" -stack_dst = { version = "0.6", features = ["unsize"], optional = true } +stack_dst = { version = "0.6", optional = true } bitflags = "1" # only used without winit [dependencies.kas-macros] diff --git a/README.md b/README.md index 0b9e8a9bd..7fe54b828 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,20 @@ Goals and status - User-customisable (*supports themes and colour schemes*) - Desktop integration (*not yet realised*) -### Stability + +### Rustc version + +KAS is compatible with **stable rustc**. Using nightly Rust is advantageous: + +- Proceedural macros emit better diagnostics. In some cases, diagnostics are + missed without nightly rustc, hence **nightly is recommended for development**. +- The `make_widget!` macro will require nightly Rust until the + `proc_macro_hygiene` feature is complete. + Usage of this macro is optional; most examples do so for convenience. +- A few other minor features are nightly-only. See *feature flags* + documentation below and in other crates' READMEs. + +### Code stability The `master` branch has frequent breaking changes. Releases will respect [semver](https://semver.org/) rules. At this point, most releases will include @@ -58,12 +71,11 @@ provide a comprehensive collection of core widgets. - Persistent widgets with embedded state - Type-safe user-defined event handlers - Robust event handling model -- (Mostly) full keyboard and touch-screen support +- Extensive support for keyboard and touch-screen control - Disabled and error states for widgets - Scalable (HiDPI) supporting fractional scaling - Theme API, simple draw API and raw graphics access -- Grid layout with spans -- Width-for-height sizing +- Automatic widget layout including grids with spans and width-for-height sizing ### Missing features diff --git a/kas-macros/Cargo.toml b/kas-macros/Cargo.toml index 1834a77dd..7907aca12 100644 --- a/kas-macros/Cargo.toml +++ b/kas-macros/Cargo.toml @@ -8,6 +8,7 @@ description = "GUI Toolkit Abstraction System (macros)" keywords = ["gui", "proc-macro"] categories = ["gui"] repository = "https://github.com/kas-gui/kas" +build = "build.rs" [lib] proc-macro = true @@ -21,3 +22,6 @@ version = "1.0.14" # We need 'extra-traits' for equality testing # We need 'full' for parsing macros within macro arguments features = ["extra-traits", "full"] + +[build-dependencies] +version_check = "0.9" diff --git a/kas-macros/README.md b/kas-macros/README.md index b1381792e..ce5cbb3c8 100644 --- a/kas-macros/README.md +++ b/kas-macros/README.md @@ -7,6 +7,18 @@ solely because procedural macros must current be in a dedicated crate. Users are advised not to depend on this library directly, but instead rely on the main KAS lib, which re-exports these macros in its API. + +Stable vs nightly +----------------- + +This crate is compatible with **stable rustc**, however, usage of **nightly** +has some benefits: + +- More macro diagnostics are emitted, resulting in better error messages + (without this, some errors may not even be reported) +- With `#![feature(proc_macro_hygiene)]`, the `make_widget!` macro may be used + + Copyright and Licence ------- diff --git a/kas-macros/build.rs b/kas-macros/build.rs new file mode 100644 index 000000000..e0cd13000 --- /dev/null +++ b/kas-macros/build.rs @@ -0,0 +1,8 @@ +fn main() { + if version_check::Channel::read() + .map(|c| c.is_nightly()) + .unwrap_or(false) + { + println!("cargo:rustc-cfg=nightly"); + } +} diff --git a/kas-macros/src/args.rs b/kas-macros/src/args.rs index 55fa491a1..f0960702e 100644 --- a/kas-macros/src/args.rs +++ b/kas-macros/src/args.rs @@ -64,6 +64,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { for attr in field.attrs.drain(..) { if attr.path == parse_quote! { layout } || attr.path == parse_quote! { handler } { // These are valid attributes according to proc_macro_derive, so we need to catch them + #[cfg(nightly)] attr.span() .unwrap() .error("invalid attribute on Widget field (applicable to struct only)") @@ -72,6 +73,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { if core_data.is_none() { core_data = Some(member(i, field.ident.clone())); } else { + #[cfg(nightly)] attr.span() .unwrap() .error("multiple fields marked with #[widget_core]") @@ -82,6 +84,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { if field.ty != parse_quote! { ::Data } && field.ty != parse_quote! { ::Data } { + #[cfg(nightly)] field .ty .span() @@ -91,6 +94,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { } layout_data = Some(member(i, field.ident.clone())); } else { + #[cfg(nightly)] attr.span() .unwrap() .error("multiple fields marked with #[layout_data]") @@ -111,6 +115,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { for attr in ast.attrs.drain(..) { if attr.path == parse_quote! { widget_core } || attr.path == parse_quote! { layout_data } { // These are valid attributes according to proc_macro_derive, so we need to catch them + #[cfg(nightly)] attr.span() .unwrap() .error("invalid attribute on Widget struct (applicable to fields only)") @@ -119,6 +124,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { if widget.is_none() { widget = Some(syn::parse2(attr.tokens)?); } else { + #[cfg(nightly)] attr.span() .unwrap() .error("multiple #[widget(..)] attributes on type") @@ -128,6 +134,7 @@ pub fn read_attrs(ast: &mut DeriveInput) -> Result { if layout.is_none() { layout = Some(syn::parse2(attr.tokens)?); } else { + #[cfg(nightly)] attr.span() .unwrap() .error("multiple #[layout(..)] attributes on type") diff --git a/kas-macros/src/lib.rs b/kas-macros/src/lib.rs index 316e7dff4..4304c705d 100644 --- a/kas-macros/src/lib.rs +++ b/kas-macros/src/lib.rs @@ -4,7 +4,7 @@ // https://www.apache.org/licenses/LICENSE-2.0 #![recursion_limit = "128"] -#![feature(proc_macro_diagnostic)] +#![cfg_attr(nightly, feature(proc_macro_diagnostic))] extern crate proc_macro; @@ -15,6 +15,7 @@ use std::collections::HashMap; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens, TokenStreamExt}; use std::fmt::Write; +#[cfg(nightly)] use syn::spanned::Spanned; use syn::Token; use syn::{parse_macro_input, parse_quote}; @@ -332,16 +333,19 @@ pub fn make_widget(input: proc_macro::TokenStream) -> proc_macro::TokenStream { syn::ImplItem::Method(syn::ImplItemMethod { sig, .. }) if sig.ident == *handler => { - if let Some(x) = x { + if let Some(_x) = x { + #[cfg(nightly)] handler .span() .unwrap() .error("multiple methods with this name") .emit(); - x.0.span() + #[cfg(nightly)] + _x.0.span() .unwrap() .error("first method with this name") .emit(); + #[cfg(nightly)] sig.ident .span() .unwrap() @@ -350,6 +354,7 @@ pub fn make_widget(input: proc_macro::TokenStream) -> proc_macro::TokenStream { return None; } if sig.inputs.len() != 3 { + #[cfg(nightly)] sig.span() .unwrap() .error("handler functions must have signature: fn handler(&mut self, mgr: &mut Manager, msg: T)") @@ -371,6 +376,7 @@ pub fn make_widget(input: proc_macro::TokenStream) -> proc_macro::TokenStream { find_handler_ty_buf.push((handler.clone(), x.1.clone())); Some(x.1) } else { + #[cfg(nightly)] handler .span() .unwrap() @@ -435,6 +441,7 @@ pub fn make_widget(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } else { // We could default to msg=VoidMsg here. If error messages weren't // so terrible this might even be a good idea! + #[cfg(nightly)] args.struct_span .unwrap() .error("make_widget: cannot discover msg type from #[handler] attr or Handler impl") diff --git a/kas-theme/Cargo.toml b/kas-theme/Cargo.toml index f4d4b7809..0a14dd4db 100644 --- a/kas-theme/Cargo.toml +++ b/kas-theme/Cargo.toml @@ -14,16 +14,19 @@ default = ["font-kit", "stack_dst"] # Use Generic Associated Types (experimental) # Currently (Feb 2020) compiler support is poor. -gat = [] +gat = ["unsize"] # Use stack_dst crate for sized unsized types stack_dst = ["kas/stack_dst", "stack_dst_"] +# Use the unstable 'unsize' feature +unsize = ["stack_dst_/unsize"] + [dependencies] font-kit = { version = "0.6.0", optional = true } lazy_static = "1.4.0" log = "0.4" -stack_dst_ = { version = "0.6", package = "stack_dst", features = ["unsize"], optional = true } +stack_dst_ = { version = "0.6", package = "stack_dst", optional = true } [dependencies.kas] path = ".." diff --git a/kas-theme/src/lib.rs b/kas-theme/src/lib.rs index 6b50356d8..16dbe297c 100644 --- a/kas-theme/src/lib.rs +++ b/kas-theme/src/lib.rs @@ -16,7 +16,7 @@ //! between themes. #![cfg_attr(feature = "gat", feature(generic_associated_types))] -#![cfg_attr(feature = "stack_dst", feature(unsize))] +#![cfg_attr(feature = "unsize", feature(unsize))] mod col; mod dim; diff --git a/kas-theme/src/multi.rs b/kas-theme/src/multi.rs index 09ce3ac6f..08fe41bac 100644 --- a/kas-theme/src/multi.rs +++ b/kas-theme/src/multi.rs @@ -6,6 +6,7 @@ //! Wrapper around mutliple themes, supporting run-time switching use std::collections::HashMap; +#[cfg(feature = "unsize")] use std::marker::Unsize; use crate::{StackDst, Theme, ThemeDst, WindowDst}; @@ -13,12 +14,17 @@ use kas::draw::{Colour, DrawHandle, DrawShared}; use kas::geom::Rect; use kas::{string::CowString, ThemeAction, ThemeApi}; +#[cfg(feature = "unsize")] +type DynTheme = StackDst>; +#[cfg(not(feature = "unsize"))] +type DynTheme = Box>; + /// Wrapper around mutliple themes, supporting run-time switching /// /// **Feature gated**: this is only available with feature `stack_dst`. pub struct MultiTheme { names: HashMap, - themes: Vec>>, + themes: Vec>, active: usize, } @@ -27,7 +33,7 @@ pub struct MultiTheme { /// Construct via [`MultiTheme::builder`]. pub struct MultiThemeBuilder { names: HashMap, - themes: Vec>>, + themes: Vec>, } impl MultiTheme { @@ -42,6 +48,10 @@ impl MultiTheme { impl MultiThemeBuilder { /// Add a theme + /// + /// Note: the constraints of this method vary depending on the `unsize` + /// feature. + #[cfg(feature = "unsize")] pub fn add, U>(mut self, name: S, theme: U) -> Self where U: Unsize>, @@ -53,6 +63,22 @@ impl MultiThemeBuilder { self } + /// Add a theme + /// + /// Note: the constraints of this method vary depending on the `unsize` + /// feature. + #[cfg(not(feature = "unsize"))] + pub fn add, T>(mut self, name: S, theme: T) -> Self + where + Draw: DrawShared, + T: ThemeDst + 'static, + { + let index = self.themes.len(); + self.names.insert(name.into(), index); + self.themes.push(Box::new(theme)); + self + } + /// Build /// /// Fails if no themes were added. diff --git a/kas-theme/src/theme_dst.rs b/kas-theme/src/theme_dst.rs index 4a2d75417..151b0895e 100644 --- a/kas-theme/src/theme_dst.rs +++ b/kas-theme/src/theme_dst.rs @@ -84,7 +84,22 @@ where } fn new_window(&self, draw: &mut D::Draw, dpi_factor: f32) -> StackDst> { - StackDst::new_or_boxed(>::new_window(self, draw, dpi_factor)) + let window = >::new_window(self, draw, dpi_factor); + #[cfg(feature = "unsize")] + { + StackDst::new_or_boxed(window) + } + #[cfg(not(feature = "unsize"))] + { + match StackDst::new_stable(window, |w| w as &dyn WindowDst) { + Ok(s) => s, + Err(window) => { + StackDst::new_stable(Box::new(window), |w| w as &dyn WindowDst) + .ok() + .expect("boxed window too big for StackDst!") + } + } + } } fn update_window(&self, window: &mut dyn WindowDst, dpi_factor: f32) { @@ -100,7 +115,16 @@ where ) -> StackDst { let window = window.as_any_mut().downcast_mut().unwrap(); let h = >::draw_handle(self, draw, window, rect); - StackDst::new_or_boxed(h) + #[cfg(feature = "unsize")] + { + StackDst::new_or_boxed(h) + } + #[cfg(not(feature = "unsize"))] + { + StackDst::new_stable(h, |h| h as &dyn DrawHandle) + .ok() + .expect("handle too big for StackDst!") + } } fn clear_colour(&self) -> Colour { @@ -115,7 +139,8 @@ impl<'a, D: DrawShared + 'static, T: Theme> ThemeDst for T { } fn new_window(&self, draw: &mut D::Draw, dpi_factor: f32) -> StackDst> { - StackDst::new_or_boxed(>::new_window(self, draw, dpi_factor)) + let window = >::new_window(self, draw, dpi_factor); + StackDst::new_or_boxed(window) } fn update_window(&self, window: &mut dyn WindowDst, dpi_factor: f32) { @@ -174,7 +199,16 @@ where { unsafe fn size_handle<'a>(&'a mut self, draw: &'a mut Draw) -> StackDst { let h = >::size_handle(self, draw); - StackDst::new_or_boxed(h) + #[cfg(feature = "unsize")] + { + StackDst::new_or_boxed(h) + } + #[cfg(not(feature = "unsize"))] + { + StackDst::new_stable(h, |h| h as &dyn SizeHandle) + .ok() + .expect("handle too big for StackDst!") + } } fn as_any_mut(&mut self) -> &mut dyn Any { diff --git a/kas-wgpu/Cargo.toml b/kas-wgpu/Cargo.toml index da3914941..b5f19bfee 100644 --- a/kas-wgpu/Cargo.toml +++ b/kas-wgpu/Cargo.toml @@ -18,6 +18,9 @@ gat = ["kas-theme/gat"] # Use stack_dst crate for sized unsized types stack_dst = ["kas-theme/stack_dst"] +# Use kas-theme's unsize feature (nightly-only) +unsize = ["kas-theme/unsize"] + [dependencies] kas = { path = "..", version = "0.3.0", features = ["winit"] } kas-theme = { path = "../kas-theme", version = "0.3.0" } diff --git a/kas-wgpu/README.md b/kas-wgpu/README.md index fb32cad0b..1e61fa58c 100644 --- a/kas-wgpu/README.md +++ b/kas-wgpu/README.md @@ -14,6 +14,7 @@ This crate has the following feature flags: - `gat`: enables usage of the Generic Associated Types feature (nightly only and currently unstable), allowing some usages of `unsafe` to be avoided. (The plan is to enable this by default once the feature is mature.) +- `unsize`: forwards this feature flag to `kas-theme` Copyright and Licence ------- diff --git a/kas-wgpu/examples/mandlebrot.rs b/kas-wgpu/examples/mandlebrot.rs index a047bb5e7..dbf9b2108 100644 --- a/kas-wgpu/examples/mandlebrot.rs +++ b/kas-wgpu/examples/mandlebrot.rs @@ -6,7 +6,6 @@ //! Mandlebrot example //! //! Demonstrates use of a custom draw pipe. -#![feature(proc_macro_hygiene)] use shaderc::{Compiler, ShaderKind}; use std::mem::size_of; @@ -555,38 +554,56 @@ impl event::Handler for Mandlebrot { } } +#[layout(grid)] +#[handler(msg = event::VoidMsg)] +#[derive(Debug, Widget)] +struct MandlebrotWindow { + #[widget_core] + core: CoreData, + #[layout_data] + layout_data: ::Data, + #[widget(cspan = 2)] + label: Label, + #[widget(row=1, halign=centre)] + iters: Label, + #[widget(row=2, handler = iter)] + slider: Slider, + #[widget(col=1, row=1, rspan=2, handler = mbrot)] + mbrot: Mandlebrot, +} +impl MandlebrotWindow { + fn new_window() -> Window { + let slider = Slider::new(0, 256, 1).with_value(64); + let mbrot = Mandlebrot::new(); + let w = MandlebrotWindow { + core: Default::default(), + layout_data: Default::default(), + label: Label::new(mbrot.loc()), + iters: Label::new("64").reserve("000"), + slider, + mbrot, + }; + Window::new("Mandlebrot", w) + } + + fn iter(&mut self, mgr: &mut Manager, iter: i32) -> VoidResponse { + self.mbrot.iter = iter; + *mgr += self.iters.set_text(format!("{}", iter)); + Response::None + } + fn mbrot(&mut self, mgr: &mut Manager, _: ()) -> VoidResponse { + *mgr += self.label.set_text(self.mbrot.loc()); + Response::None + } +} + fn main() -> Result<(), kas_wgpu::Error> { env_logger::init(); - let mbrot = Mandlebrot::new(); - let slider = Slider::new(0, 256, 1).with_value(64); - - let window = make_widget! { - #[layout(grid)] - #[handler(msg = event::VoidMsg)] - struct { - #[widget(cspan=2)] label: Label = Label::new(mbrot.loc()), - #[widget(row=1, halign=centre)] iters: Label = Label::new("64").reserve("000"), - #[widget(row=2, handler = iter)] _: Slider = slider, - #[widget(col=1, row=1, rspan=2, handler = mbrot)] mbrot: Mandlebrot = mbrot, - } - impl { - fn iter(&mut self, mgr: &mut Manager, iter: i32) -> VoidResponse { - self.mbrot.iter = iter; - *mgr += self.iters.set_text(format!("{}", iter)); - Response::None - } - fn mbrot(&mut self, mgr: &mut Manager, _: ()) -> VoidResponse { - *mgr += self.label.set_text(self.mbrot.loc()); - Response::None - } - } - }; - let window = Window::new("Mandlebrot", window); - let mut theme = kas_theme::FlatTheme::new(); theme.set_colours("dark"); + let mut toolkit = kas_wgpu::Toolkit::new_custom(PipeBuilder, theme, Options::from_env())?; - toolkit.add(window)?; + toolkit.add(MandlebrotWindow::new_window())?; toolkit.run() }