Skip to content

Commit

Permalink
feat(macros): 🎸 the part_writer! macro
Browse files Browse the repository at this point in the history
Creating a writer from a mutable reference of a writer will
automatically switch to the built-in widget if its reference is a
built-in field or method.
  • Loading branch information
M-Adoo committed Oct 25, 2024
1 parent f605a3c commit 04ca2d7
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 64 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
8 changes: 3 additions & 5 deletions core/src/animation/stagger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
);
Expand Down
2 changes: 2 additions & 0 deletions macros/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub enum Error {
WatchNothing(Span),
RdlAtSyntax { at: Span, follow: Option<Span> },
IdentNotFollowDollar(Span),

Syn(syn::Error),
}

Expand Down Expand Up @@ -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(),
}
}
Expand Down
29 changes: 14 additions & 15 deletions macros/src/fn_widget_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<BodyExpr>(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::<BodyExpr>(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)
}
21 changes: 19 additions & 2 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
117 changes: 117 additions & 0 deletions macros/src/part_writer.rs
Original file line number Diff line number Diff line change
@@ -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::<PartWriter>(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<Token![&]>,
mutability: Option<Token![mut]>,
writer: Ident,
dot: Token![.],
part_expr: PartExpr,
}

enum PartExpr {
Member(Member),
Method {
method: Ident,
turbofish: Option<AngleBracketedGenericArguments>,
paren_token: Paren,
args: Punctuated<Expr, Token![,]>,
},
}

impl Parse for PartWriter {
fn parse(input: ParseStream) -> Result<Self> {
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::<Ident>()?;
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))
}
}
}
Loading

0 comments on commit 04ca2d7

Please sign in to comment.