Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduced a builder pattern interface (#275) #809

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

simongarnier
Copy link

Objective

Fixes #275 by providing a builder pattern interface to create nested styles which can then be "materialize" (made concrete? not sure of the lingo to use here) into tree nodes by passing a TaffyTree into the build method of the builder.

Changes that will affect external library users must update RELEASES.md before they will be merged.

Might be good to update the RELEASE.md since this adds a new API to interact with Taffy. Happy to add this to this PR if required!

Context

  • Per the discussion on Provide a builder pattern interface to the construction layout tree #275, I focused my efforts on providing a user-friendly interface that does not require the caller to use macros or closures.
  • To avoid repeating myself, I implemented a declarative macro (gen_builder!, taking suggestions for a better name) to generate the builder. This macro takes a name for the resulting struct and a list of Style fields that should be available on the builder. Each field can also be marked with a cfg entry, useful for conditional compilation based on features.
  • So far all fields are made available on the builder. I also added shortcuts for width and height and 2 constructors, column and row that are presets for the appropriate FlexDirection.
  • I took the approach that StyleBuilder should take in references to other StyleBuilder as children. This avoids having to build children style in advance and has the advantage of being consistent across root and child styles.

Feedback wanted

  • I wasn't sure about how best to provide NodeIds back to the caller for child nodes. In the end, I landed on taking inspiration from the NodeRef solution mentioned in Provide a builder pattern interface to the construction layout tree #275. The main difference is that I'm using a Cell<_> instead of a RefCell<_> to hold the NodeId, since NodeId implement copy. I'm looking for feedback here regarding how this is implemented and also if another pattern for result feedback would be desired over this.
  • I'm a fairly new rust programmer and this is my first time implementing a declarative macro. Feedback would be appreciated on that aspect, and on the rust programming language at large as well.

Copy link
Collaborator

@nicoburns nicoburns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty impressed with this PR. It's not a lot of lines of code for the functionality it provides. I've left some comments on specific parts of the code, but in addition to that I have a couple of general points to make:

  • I feel like you are potentially mixing too concepts here:

    • A StyleBuilder (helper methods to set Style fields)
    • A TreeBuilder (the stuff around children)

    This isn't necessarily a problem. Just something to bare in mind. Potentially the setting methods could be added directly to Style and StyleBuilder could be renamed to TreeBuilder?

  • I think shorthand methods are an important part of "builder" functionality. So I think the .margin() method ought to accept LengthPercentage and set all four margins to the single specified value (like margin shorthand in CSS) rather than accepting a Rect<LengthPercentage>. There should then be separate margin_left, margin_top, etc methods. And probably also margin_x (or margin_horizontal) and y/vertical too.

  • While I'm impressed with the macro, I wonder if it might be worth switching to a code generation solution (a rust script that generates rust code using a string templating library (e.g. tera)). The advantage of that would be that the same "API definition" could potentially be shared by this builder and future FFI crates which will require very similar builders (See draft C API and wasm API PRs).

    What do you think? We could also potentially land the macro-based solution and then switch to codegen later.

Comment on lines 22 to 38
#[derive(Debug, Clone, Default)]
pub struct NodeIdRef(Rc<Cell<Option<NodeId>>>);

impl NodeIdRef {
/// Create an empty [`NodeIdRef`].
pub fn new() -> Self {
Self(Rc::new(Cell::new(None)))
}

/// Set the [`NodeId`].
fn set(&self, node_id: NodeId) {
self.0.set(Some(node_id));
}

/// Get a copy of the inner [`NodeId`], if any is present.
pub fn get(&self) -> Option<NodeId> {
self.0.get()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, but I think this would work with a simple mutable reference instead of the Rc<Cell<>>?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need Rc<Cell<_>> here to provide multiple ownership via the clone method and interior mutability. This is required because the call site needs to own the NodeIdRef, but also needs to be able to pass it into the builder, which then modifies the content of the Cell<_>. Let me know if that makes sense, happy to implement this with a mutable reference if you have a specific way this can be achieved!

Copy link
Collaborator

@nicoburns nicoburns Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simongarnier this appears to work (compiles at least) 32d90c2 (you could also get rid of the NodeIdRef type and use &mut Option<NodeId> directly if you wanted).

Comment on lines 83 to 86
$(
$(#[cfg($($cfg)+)])?
$field: Option<$type>,
)*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having Option<T> fields for all Style fields, why not just have a single style: Style field here? It could be initialised with Style::default() and then written to directly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, doing this also removes the need for having a build_style method.

@simongarnier simongarnier force-pushed the builder-pattern-interface branch from e4325c6 to eee08c1 Compare March 6, 2025 03:40
@simongarnier
Copy link
Author

simongarnier commented Mar 7, 2025

This isn't necessarily a problem. Just something to bare in mind. Potentially the setting methods could be added directly to Style and StyleBuilder could be renamed to TreeBuilder?

The first approach I took was to create a StyleBuilder and then have a TreeBuilder where each node owned a StyleBuilder. The issue was that the API was not as ergonomic, since the call site needed to populate each TreeBuilder with a StyleBuilder, which led to either having a callback to set the styles, or having to create the StyleBuilder separately. I think having the method to set styles and add child nodes on the same struct is best in term of ease of use, but I'm also open to implementing the callback solution.

I do think TreeBuilder would be a better name for what is currently called StyleBuilder, since it is in fact building a tree of node and not just a Style.

I think shorthand methods are an important part of "builder" functionality. So I think the .margin() method ought to accept LengthPercentage and set all four margins to the single specified value (like margin shorthand in CSS) rather than accepting a Rect. There should then be separate margin_left, margin_top, etc methods. And probably also margin_x (or margin_horizontal) and y/vertical too.

I was wondering about which shorthand should be implement, maybe I can get some inspiration from what CSS offers in terms of shorthand. I'll do a pass to add the margin ones and see what I can find on the CSS side.

While I'm impressed with the macro, I wonder if it might be worth switching to a code generation solution (a rust script that generates rust code using a string templating library (e.g. tera)). The advantage of that would be that the same "API definition" could potentially be shared by this builder and future FFI crates which will require very similar builders (See draft C API and wasm API PRs).

I think code generation could be use instead of a macro for this use case, however I don't know how that works on the documentation end. Code generation can probably be done before the docs are generated. Like you said, I don't think this is a one way door and this can be switched to code generation when the need for it arises, i.e. to generate bindings from a single API definition.

Let me know what you think, I'll focus on shorthand methods for now. Thanks for the review by the way!

@simongarnier simongarnier force-pushed the builder-pattern-interface branch from 3a19e75 to 9439948 Compare March 11, 2025 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide a builder pattern interface to the construction layout tree
2 participants