diff --git a/CHANGELOG.md b/CHANGELOG.md index f1eb25b36..2580913ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [@Unreleased] - @ReleaseDate +### Features + +- **macros**: Added the `part_writer!` macro to generate a partial writer from a mutable reference of a writer. (#642 @M-Adoo) + ### Fixed - **core**: Setting the theme before running the app results in the tree being constructed twice. (#637, @M-Adoo) diff --git a/Cargo.toml b/Cargo.toml index fefed420e..1c85bd472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ rust-version = "1.81.0" [workspace.dependencies] ahash = "0.8.11" arboard = "3.2.0" -bitflags = "2.0.0" +bitflags = "2.6.0" blake3 = "1.3.3" colored = "2.0.0" derive_more = "1.0.0" @@ -52,8 +52,7 @@ fontdb = "0.22.0" futures = "0.3.26" guillotiere = "0.6.0" image = { version = "0.24.5", default-features = false } -# Version 4.7.2 of the iterator contains a bug. We are awaiting the release of patch #117 to address this issue. -indextree = { git = "https://github.com/saschagrunert/indextree.git", rev = "f75bdbb1dfbd5c63519fabedd0127dcbc8130052"} +indextree = "4.7.3" log = "0.4.14" lyon_algorithms = "1.0.1" lyon_geom = "1.0.1" diff --git a/core/src/animation/stagger.rs b/core/src/animation/stagger.rs index 113a58d7d..9c8c68f3f 100644 --- a/core/src/animation/stagger.rs +++ b/core/src/animation/stagger.rs @@ -230,18 +230,16 @@ mod tests { WidgetTester::new(fn_widget! { let stagger = Stagger::new(Duration::from_millis(100), transitions::EASE_IN.of(ctx!())); let mut mock_box = @MockBox { size: Size::new(100., 100.) }; - let opacity = mock_box - .get_opacity_widget() - .map_writer(|w| PartData::from_ref_mut(&mut w.opacity)); + let animate = @Animate { transition: transitions::EASE_IN.of(ctx!()), - state: opacity, + state: part_writer!(&mut mock_box.opacity), from: 0., }; stagger.write().push_animation(animate); stagger.write().push_state( - mock_box.map_writer(|w| PartData::from_ref_mut(&mut w.size)), + part_writer!(&mut mock_box.size), Size::new(200., 200.), ctx!() ); diff --git a/macros/src/error.rs b/macros/src/error.rs index 87e6e3c58..858eefdf9 100644 --- a/macros/src/error.rs +++ b/macros/src/error.rs @@ -6,6 +6,7 @@ pub enum Error { WatchNothing(Span), RdlAtSyntax { at: Span, follow: Option }, IdentNotFollowDollar(Span), + Syn(syn::Error), } @@ -34,6 +35,7 @@ impl Error { Error::IdentNotFollowDollar(span) => { quote_spanned! { *span => compile_error!("Syntax error: expected an identifier after `$`"); } } + Error::Syn(err) => err.to_compile_error(), } } diff --git a/macros/src/fn_widget_macro.rs b/macros/src/fn_widget_macro.rs index b1ae8698f..32d3da790 100644 --- a/macros/src/fn_widget_macro.rs +++ b/macros/src/fn_widget_macro.rs @@ -9,22 +9,21 @@ use crate::{ }; pub(crate) fn gen_code(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream { - let res = symbol_to_macro(input) - .and_then(|input| syn::parse2::(input).map_err(Into::into)) - .map(|body| { - refs_ctx.new_dollar_scope(None); - let stmts: Vec<_> = body - .0 - .into_iter() - .map(|s| refs_ctx.fold_stmt(s)) - .collect(); + let res = symbol_to_macro(input).and_then(|input| { + let body = syn::parse2::(input)?; + refs_ctx.new_dollar_scope(None); + let stmts: Vec<_> = body + .0 + .into_iter() + .map(|s| refs_ctx.fold_stmt(s)) + .collect(); - refs_ctx.current_dollar_scope_mut().used_ctx = false; - let _ = refs_ctx.pop_dollar_scope(false); - quote! { - move |ctx!(): &mut BuildCtx| -> Widget { #(#stmts)*.into_widget() } - } - }); + refs_ctx.current_dollar_scope_mut().used_ctx = false; + let _ = refs_ctx.pop_dollar_scope(false); + Ok(quote! { + move |ctx!(): &mut BuildCtx| -> Widget { #(#stmts)*.into_widget() } + }) + }); result_to_token_stream(res) } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 6168eba7b..98cba6d1e 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -5,6 +5,7 @@ extern crate proc_macro; mod declare_derive; mod lerp_derive; +mod part_writer; mod util; use proc_macro::TokenStream; use quote::quote; @@ -177,8 +178,8 @@ pub fn style_class(input: TokenStream) -> TokenStream { .into() } -/// This macro is utilized for generating a `Pipe` object that actively monitors -/// the expression's result. +/// A shorthand macro for `pipe!` can be utilized as follows: +/// `pipe!(...).value_chain(|s| s.distinct_until_changed().box_it())`. /// /// It triggers when the new result differs from the previous one. The `$` /// symbol denotes the state reference and automatically subscribes to any @@ -233,6 +234,22 @@ pub fn watch(input: TokenStream) -> TokenStream { watch_macro::gen_code(input.into(), &mut DollarRefsCtx::top_level()).into() } +/// The `part_writer` macro creates a partial writer from a mutable reference of +/// a writer. +/// +/// This macro specifically accepts simple expressions to indicate the partial +/// of the writer, as shown in the following patterns: +/// +/// - For a field: `part_writer!(&mut writer.xxx)` +/// - For a method returning a mutable reference: `part_writer!(writer.xxx())`. +/// +/// Since it operates on a writer and not a state reference of the writer, the +/// use of `$` is unnecessary. +#[proc_macro] +pub fn part_writer(input: TokenStream) -> TokenStream { + part_writer::gen_code(input.into(), &mut DollarRefsCtx::top_level()).into() +} + /// Includes an SVG file as an `Svg`. /// /// The file is located relative to the current crate (similar to the location diff --git a/macros/src/part_writer.rs b/macros/src/part_writer.rs new file mode 100644 index 000000000..8cda632d4 --- /dev/null +++ b/macros/src/part_writer.rs @@ -0,0 +1,117 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{ToTokens, quote, quote_spanned}; +use syn::{ + AngleBracketedGenericArguments, Expr, Member, Result, Token, parenthesized, + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::Paren, +}; + +use crate::{ + symbol_process::{DollarRef, DollarRefsCtx, DollarUsedInfo}, + variable_names::{BUILTIN_INFOS, BuiltinMemberType}, +}; + +pub fn gen_code(input: TokenStream, refs_ctx: &mut DollarRefsCtx) -> TokenStream { + match syn::parse2::(input) { + Ok(part) => { + let info = part.writer_info(refs_ctx); + let tokens = part.gen_tokens(&info, refs_ctx); + refs_ctx.add_dollar_ref(info); + tokens + } + Err(err) => err.to_compile_error(), + } +} + +struct PartWriter { + and_token: Option, + mutability: Option, + writer: Ident, + dot: Token![.], + part_expr: PartExpr, +} + +enum PartExpr { + Member(Member), + Method { + method: Ident, + turbofish: Option, + paren_token: Paren, + args: Punctuated, + }, +} + +impl Parse for PartWriter { + fn parse(input: ParseStream) -> Result { + let and_token = input.parse()?; + let mutability = input.parse()?; + let writer = input.parse()?; + let dot = input.parse()?; + + let part_expr = if input.peek(syn::LitInt) { + PartExpr::Member(input.parse()?) + } else { + let name = input.parse::()?; + if input.is_empty() { + PartExpr::Member(Member::Named(name)) + } else { + let turbofish = if input.peek(Token![::]) { + Some(AngleBracketedGenericArguments::parse_turbofish(input)?) + } else { + None + }; + let content; + PartExpr::Method { + method: name, + turbofish, + paren_token: parenthesized!(content in input), + args: content.parse_terminated(Expr::parse, Token![,])?, + } + } + }; + Ok(Self { and_token, mutability, writer, dot, part_expr }) + } +} + +impl PartWriter { + fn writer_info(&self, refs_ctx: &DollarRefsCtx) -> DollarRef { + let builtin_info = match &self.part_expr { + PartExpr::Member(Member::Named(member)) => BUILTIN_INFOS + .get(&member.to_string()) + .filter(|info| info.mem_ty == BuiltinMemberType::Field), + PartExpr::Method { method, .. } => BUILTIN_INFOS + .get(&method.to_string()) + .filter(|info| info.mem_ty == BuiltinMemberType::Method), + _ => None, + }; + if let Some(info) = builtin_info { + refs_ctx.builtin_dollar_ref(self.writer.clone(), info, DollarUsedInfo::Writer) + } else { + DollarRef { name: self.writer.clone(), builtin: None, used: DollarUsedInfo::Writer } + } + } + + fn gen_tokens(&self, writer_info: &DollarRef, refs_ctx: &DollarRefsCtx) -> TokenStream { + let Self { and_token, mutability, writer, dot, part_expr } = self; + let part_expr = match part_expr { + PartExpr::Member(member) => member.to_token_stream(), + PartExpr::Method { method, turbofish, paren_token, args } => { + let mut tokens = quote! {}; + method.to_tokens(&mut tokens); + turbofish.to_tokens(&mut tokens); + paren_token.surround(&mut tokens, |tokens| args.to_tokens(tokens)); + tokens + } + }; + let host = if writer_info.builtin.is_some() { + refs_ctx.builtin_host_tokens(writer_info) + } else { + writer.to_token_stream() + }; + + quote_spanned! { writer.span() => + #host #dot map_writer(|w| PartData::from_ref_mut(#and_token #mutability w #dot #part_expr)) + } + } +} diff --git a/macros/src/symbol_process.rs b/macros/src/symbol_process.rs index 98f8b912a..a6f698b2f 100644 --- a/macros/src/symbol_process.rs +++ b/macros/src/symbol_process.rs @@ -24,6 +24,7 @@ pub const KW_RDL: &str = "rdl"; pub const KW_PIPE: &str = "pipe"; pub const KW_DISTINCT_PIPE: &str = "distinct_pipe"; pub const KW_WATCH: &str = "watch"; +pub const KW_PART_WRITER: &str = "part_writer"; pub const KW_FN_WIDGET: &str = "fn_widget"; pub use tokens_pre_process::*; @@ -245,11 +246,10 @@ impl Fold for DollarRefsCtx { let ExprField { base, member, .. } = &mut i; if let Member::Named(member) = member { - let dollar = BUILTIN_INFOS - .get(member.to_string().as_str()) - .filter(|info| info.mem_ty == BuiltinMemberType::Field) - .and_then(|info| self.replace_builtin_ident(&mut *base, info)); - if dollar.is_some() { + let info = BUILTIN_INFOS.get(&member.to_string()); + if info.map_or(false, |info| { + info.mem_ty == BuiltinMemberType::Field && self.replace_builtin_host(&mut *base, info) + }) { return i; } } @@ -259,12 +259,10 @@ impl Fold for DollarRefsCtx { fn fold_expr_method_call(&mut self, mut i: ExprMethodCall) -> ExprMethodCall { // fold builtin method on state - if BUILTIN_INFOS - .get(&i.method.to_string()) - .filter(|info| info.mem_ty == BuiltinMemberType::Method) - .and_then(|info| self.replace_builtin_ident(&mut i.receiver, info)) - .is_some() - { + let info = BUILTIN_INFOS.get(&i.method.to_string()); + if info.map_or(false, |info| { + info.mem_ty == BuiltinMemberType::Method && self.replace_builtin_host(&mut i.receiver, info) + }) { return i; } @@ -294,6 +292,9 @@ impl Fold for DollarRefsCtx { } else if mac.path.is_ident(KW_WATCH) { mac.tokens = crate::watch_macro::gen_code(mac.tokens, self); mark_macro_expanded(&mut mac); + } else if mac.path.is_ident(KW_PART_WRITER) { + mac.tokens = crate::part_writer::gen_code(mac.tokens, self); + mark_macro_expanded(&mut mac); } else if mac.path.is_ident(KW_PIPE) { mac.tokens = crate::pipe_macro::gen_code(mac.tokens, self); mark_macro_expanded(&mut mac); @@ -490,38 +491,38 @@ impl DollarRefsCtx { } } + pub fn builtin_dollar_ref( + &self, host: Ident, info: &BuiltinMember, used: DollarUsedInfo, + ) -> DollarRef { + // When a builtin widget captured by a `move |_| {...}` closure, we need split + // the builtin widget from the `FatObj` so we only capture the builtin part that + // we used. + let name = ribir_suffix_variable(&host, info.var_name); + let get_builtin = info.get_builtin_widget_method(host.span()); + let run_before_clone = info + .run_before_clone_method(host.span()) + .into_iter() + .collect(); + + let builtin = Some(BuiltinInfo { host, get_builtin, run_before_clone }); + DollarRef { name, builtin, used } + } + fn mark_used_ctx(&mut self) { self.current_dollar_scope_mut().used_ctx = true; } - fn replace_builtin_ident( - &mut self, caller: &mut Expr, info: &BuiltinMember, - ) -> Option<&DollarRef> { + fn replace_builtin_host(&mut self, caller: &mut Expr, info: &BuiltinMember) -> bool { let mut used = DollarUsedInfo::Reader; - let e = if let Expr::MethodCall(m) = caller { - if is_state_write_method(&m.method) { + let e = match caller { + Expr::MethodCall(m) if is_state_write_method(&m.method) => { used = DollarUsedInfo::Writer; &mut *m.receiver - } else { - caller } - } else { - caller + e => e, }; + let Expr::Macro(m) = e else { return false }; - let Expr::Macro(m) = e else { return None }; - let DollarMacro { name: host, .. } = parse_dollar_macro(&m.mac)?; - - // When a builtin widget captured by a `move |_| {...}` closure, we need split - // the builtin widget from the `FatObj` so we only capture the builtin part that - // we used. - let name = ribir_suffix_variable(&host, info.var_name); - let get_builtin = Ident::new(&format!("get_{}_widget", info.var_name), host.span()); - let mut run_before_clone = SmallVec::default(); - if let Some(method) = info.run_before_clone { - run_before_clone.push(Ident::new(method, host.span())); - } - Ident::new(&format!("get_{}_widget", info.var_name), host.span()); - let builtin = Some(BuiltinInfo { host, get_builtin, run_before_clone }); - let dollar_ref = DollarRef { name, builtin, used }; + let Some(DollarMacro { name: host, .. }) = parse_dollar_macro(&m.mac) else { return false }; + let dollar_ref = self.builtin_dollar_ref(host, info, used); let state = self.builtin_host_tokens(&dollar_ref); m.mac.tokens = if dollar_ref.used == DollarUsedInfo::Writer { @@ -530,9 +531,9 @@ impl DollarRefsCtx { expand_read(state) }; mark_macro_expanded(&mut m.mac); - self.add_dollar_ref(dollar_ref); - self.current_dollar_scope().last() + + true } fn new_local_var(&mut self, name: &Ident) { @@ -543,7 +544,7 @@ impl DollarRefsCtx { .push(name.clone()) } - fn add_dollar_ref(&mut self, dollar_ref: DollarRef) { + pub fn add_dollar_ref(&mut self, dollar_ref: DollarRef) { // local variable is not a outside reference. if self.is_capture_var(dollar_ref.host()) { let scope = self.current_dollar_scope_mut(); @@ -680,7 +681,7 @@ impl<'a> Drop for StackGuard<'a> { } impl Default for DollarRefsCtx { - fn default() -> Self { Self { scopes: smallvec![], variable_stacks: vec![vec![]] } } + fn default() -> Self { Self { scopes: smallvec![<_>::default()], variable_stacks: vec![vec![]] } } } fn is_state_write_method(m: &Ident) -> bool { m == "write" || m == "silent" || m == "shallow" } diff --git a/macros/src/variable_names.rs b/macros/src/variable_names.rs index 0d7d789ba..3affad75c 100644 --- a/macros/src/variable_names.rs +++ b/macros/src/variable_names.rs @@ -1,3 +1,4 @@ +use proc_macro2::Span; use syn::Ident; pub(crate) const AVOID_CONFLICT_SUFFIX: &str = "ಠ_ಠ"; @@ -20,6 +21,19 @@ pub struct BuiltinMember { pub run_before_clone: Option<&'static str>, } +impl BuiltinMember { + pub fn get_builtin_widget_method(&self, span: Span) -> Ident { + Ident::new(&format!("get_{}_widget", self.var_name), span) + } + + pub fn run_before_clone_method(&self, span: Span) -> Option { + self + .run_before_clone + .as_ref() + .map(|method| Ident::new(method, span)) + } +} + use phf::phf_map; use self::BuiltinMemberType::*;