Skip to content

[DG-243] Fix button schema padding mismatch with CWButton defaults#476

Open
carbonFibreCode wants to merge 3 commits into
mainfrom
arun/dg-243
Open

[DG-243] Fix button schema padding mismatch with CWButton defaults#476
carbonFibreCode wants to merge 3 commits into
mainfrom
arun/dg-243

Conversation

@carbonFibreCode
Copy link
Copy Markdown
Collaborator

No description provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 20, 2025

Walkthrough

Compute button disabled state from props/onClick; derive per-state background/text/leading/trailing icon colors; extend ButtonStyle with fixedSize/minimumSize/tapTargetSize/visualDensity; update onPressed to respect disabled and call onClick; refactor content builder to accept explicit color overrides and conditionally render icons.

Changes

Cohort / File(s) Change Summary
Button widget: per-state styling & content refactor
lib/src/framework/widgets/button.dart
Compute isDisabled from props and onClick; derive backgroundColor, textColor, leadingIconColor, trailingIconColor from disabled or default styles; replace per-state background resolver with direct value; extend ButtonStyle construction (fixedSize, minimumSize, tapTargetSize, visualDensity); update onPressed to respect isDisabled and trigger onClick with triggerType: 'onPressed'; refactor _buildContent(RenderPayload, {String? textColor, String? leadingIconColor, String? trailingIconColor}) to apply color overrides and render leading/trailing icons conditionally, preserving layout with gaps when icons are absent.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions fixing button schema padding mismatch, but the actual changes involve comprehensive button styling refactoring (disabled states, color overrides, icon rendering), not just padding fixes. Update the title to reflect the main changes, such as 'Refactor button styling with per-state colors and conditional icon rendering' or 'Add per-state button styling and icon color overrides'.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relatedness to the changeset. Add a description explaining the button styling refactoring, the rationale for changes, and how the schema padding issue relates to these broader modifications.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch arun/dg-243

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
lib/src/framework/widgets/button.dart (1)

91-93: Consider simplifying the isDisabled logic.

The condition checks buttonState == ButtonState.disabledState in addition to isDisabled and onClick == null. Since the button's visual styling is already controlled by buttonState, this additional check may be redundant for interactivity purposes.

If the intention is to disable interaction whenever buttonState is explicitly set to disabled, consider whether this should override the isDisabled prop or if the logic can be simplified:

 final isDisabled = payload.eval<bool>(props.get('isDisabled')) ??
-    props.get('onClick') == null ||
-        buttonState == ButtonState.disabledState;
+    props.get('onClick') == null;

Alternatively, document the interaction between isDisabled, onClick, and buttonState for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c50284b and 2068089.

📒 Files selected for processing (1)
  • lib/src/framework/widgets/button.dart (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-format-and-analyse
🔇 Additional comments (4)
lib/src/framework/widgets/button.dart (4)

20-23: LGTM!

The ButtonState enum is well-defined and provides clear state management for the button.


184-193: LGTM!

The _getButtonState helper method correctly maps string values to the ButtonState enum with appropriate defaults.


66-69: Good fix for padding mismatch.

The addition of tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.standard, and minimumSize: Size.zero correctly addresses the PR objective of matching CWButton defaults. These settings remove Flutter's default padding and allow precise control over button sizing.


143-143: No issues found - null handling is correct.

The ExprOr.fromJson<T>() method properly handles null values. It accepts Object? as a parameter and includes an explicit null check at line 83 of types.dart that returns null gracefully. Both leadingIconColor and trailingIconColor are nullable String? values that may be passed to this method, and the method's return type ExprOr<T>? correctly handles both cases (non-null wrapped value or null).

Comment thread lib/src/framework/widgets/button.dart Outdated
Comment on lines +41 to +44
final backgroundColor = buttonState == ButtonState.defaultState
? payload.evalColor(defaultStyleJson.getString('backgroundColor'))
: (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
const Color(0xFFE0E0E0)); // AppColorsV2.contentDisabled fallback
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null fallback for default state backgroundColor.

The backgroundColor only has a fallback (Color(0xFFE0E0E0)) for the disabled state. If defaultStyleJson.getString('backgroundColor') returns null for the default state, this could result in a null backgroundColor being passed to the button style, potentially causing unexpected rendering behavior.

Consider adding a fallback for the default state as well:

 final backgroundColor = buttonState == ButtonState.defaultState
-    ? payload.evalColor(defaultStyleJson.getString('backgroundColor'))
+    ? (payload.evalColor(defaultStyleJson.getString('backgroundColor')) ??
+        Colors.blue) // or appropriate default color
     : (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
         const Color(0xFFE0E0E0));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final backgroundColor = buttonState == ButtonState.defaultState
? payload.evalColor(defaultStyleJson.getString('backgroundColor'))
: (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
const Color(0xFFE0E0E0)); // AppColorsV2.contentDisabled fallback
final backgroundColor = buttonState == ButtonState.defaultState
? (payload.evalColor(defaultStyleJson.getString('backgroundColor')) ??
Colors.blue) // or appropriate default color
: (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
const Color(0xFFE0E0E0)); // AppColorsV2.contentDisabled fallback
🤖 Prompt for AI Agents
In lib/src/framework/widgets/button.dart around lines 41 to 44, the
default-state backgroundColor uses
payload.evalColor(defaultStyleJson.getString('backgroundColor')) without a null
fallback, so if getString returns null the backgroundColor may be null; update
the ternary branch to provide a fallback (e.g. wrap the evalColor(...) with a
null-coalescing fallback like ?? const Color(0xFFE0E0E0) or
AppColorsV2.contentDisabled) so both default and disabled branches always
produce a non-null Color.

Comment thread lib/src/framework/widgets/button.dart Outdated
Comment on lines 74 to 76
fixedSize: width != null || height != null
? WidgetStateProperty.all(
Size(width ?? double.infinity, height ?? double.infinity))
? Size(width ?? double.infinity, height ?? double.infinity)
: null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix Size construction to avoid infinite dimensions.

Using double.infinity as a fallback for width or height can cause layout issues. If only one dimension is provided, the other should not default to infinity, as this will force the button to expand infinitely in that dimension.

Apply this diff to fix the issue:

-      fixedSize: width != null || height != null
-          ? Size(width ?? double.infinity, height ?? double.infinity)
-          : null,
+      fixedSize: width != null && height != null
+          ? Size(width, height)
+          : null,

Alternatively, if you want to support setting only width or height individually, consider using minimumSize or maximumSize constraints instead of fixedSize.

🤖 Prompt for AI Agents
In lib/src/framework/widgets/button.dart around lines 74 to 76, the code
constructs a Size using double.infinity for the missing dimension causing
infinite layout; change the logic so fixedSize is only set when both width and
height are provided (use Size(width, height) then), and if only one dimension is
supplied do NOT use double.infinity — instead leave fixedSize null and apply a
minimumSize/maximumSize (or another appropriate constraint) for the
single-dimension case so the button does not expand infinitely.

Comment on lines 121 to 122
final JsonLike localProps =
jsonDecode(jsonEncode(props.value)) as JsonLike? ?? {};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Performance concern: Avoid expensive JSON serialization for props cloning.

Using jsonDecode(jsonEncode(props.value)) for deep cloning is computationally expensive and will be executed on every render. This could impact performance, especially when many buttons are rendered in lists or grids.

Consider these alternatives:

  1. Create a dedicated method to clone the specific properties you need to modify
  2. Use a more efficient deep copy mechanism
  3. Build a new Props object with only the overridden values

Example approach:

// Instead of deep cloning entire props, create a modified version selectively
final textProps = as$<JsonLike>(props.value['text']).maybe(TextProps.fromJson) ?? TextProps();
final modifiedTextProps = textColor != null 
    ? textProps.copyWith(textStyle: textProps.textStyle?.copyWith(color: textColor))
    : textProps;

text = VWText(
  props: modifiedTextProps,
  commonProps: null,
  parent: this,
).toWidget(payload);
🤖 Prompt for AI Agents
In lib/src/framework/widgets/button.dart around lines 121-122, the code uses
jsonDecode(jsonEncode(props.value)) to deep-clone props which is expensive on
every render; replace this expensive full JSON round-trip by constructing a new
props object with only the fields you need to change (or implement a
light-weight clone/merge helper) — extract the specific child props (e.g., text
props), apply copyWith or manual field overrides to create a modified props
instance, and pass that to the child widget so you avoid serializing the entire
props map on each render.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
lib/src/framework/widgets/button.dart (2)

37-40: ButtonStyle sizing & color fallbacks still have edge cases (null background + infinite dimensions)

There are a few things worth tightening up in the style construction:

  1. Enabled backgroundColor may still be null (duplicate of earlier review)
    The disabled path has a safe fallback, but the enabled path does not. If backgroundColor is omitted from defaultStyle, WidgetStateProperty.all(backgroundColor) may receive null and yield unexpected visual behavior.

  • final backgroundColor = isDisabled
  •    ? (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
    
  •        const Color(0xFFE0E0E0))
    
  •    : payload.evalColor(defaultStyleJson.getString('backgroundColor'));
    
  • final backgroundColor = isDisabled
  •    ? (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
    
  •        const Color(0xFFE0E0E0))
    
  •    : (payload.evalColor(defaultStyleJson.getString('backgroundColor')) ??
    
  •        const Color(0xFFE0E0E0)); // or a design‑system primary
    
    Adjust the fallback color to whatever your design system expects.  
    
    
    
  1. fixedSize with double.infinity can cause layout issues (duplicate of earlier review)
    Using Size(width ?? double.infinity, height ?? double.infinity) forces the unconstrained dimension to be infinite, which can blow up layouts in many flex / scroll contexts.

    A safer pattern is to only set fixedSize when both dimensions are provided, and otherwise rely on minimumSize / intrinsic sizing:

  •  fixedSize: (width != null || height != null)
    
  •      ? WidgetStateProperty.all(
    
  •          Size(width ?? double.infinity, height ?? double.infinity),
    
  •        )
    
  •      : null,
    
  •  fixedSize: (width != null && height != null)
    
  •      ? WidgetStateProperty.all(Size(width, height))
    
  •      : null,
    
    
    
  1. minimumSize: Size.zero + tapTargetSize: shrinkWrap reduces touch targets a lot
    This combination effectively removes Material’s default 48×48 tap target. If that’s intentional and matches the product’s a11y guidelines, fine; otherwise you may want to keep the Material defaults or apply a smaller but non‑zero minimum to avoid hard‑to‑tap buttons, especially on mobile.

    For example, if you only want to shrink horizontally, you can keep a vertical minimum:

  •  minimumSize: WidgetStateProperty.all(Size.zero),
    
  •  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
    
  •  minimumSize: WidgetStateProperty.all(const Size(0, 40)), // example
    
  •  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
    
    
    
    
    
    
    
    

Also applies to: 59-60, 61-67, 72-73, 75-83


111-112: Avoid jsonEncode/jsonDecode deep clone inside _buildContent

final JsonLike localProps = jsonDecode(jsonEncode(props.value)) as JsonLike? ?? {}; does a full JSON round‑trip on every render, which is relatively expensive for a frequently built widget like a button, especially in lists or grids.

Since _buildContent only needs to override a few specific sub‑trees (text props and leading/trailing icon props), you can avoid cloning the entire props map and instead construct just the parts you need:

final textJson = as$<JsonLike>(props.value['text']);
final textProps =
    textJson.maybe(TextProps.fromJson) ?? TextProps();
final effectiveTextProps = textColor != null
    ? textProps.copyWith(
        textStyle: textProps.textStyle?.copyWith(color: textColor),
      )
    : textProps;

// Similarly for leading/trailing icons using IconProps.fromJson + copyWith

Then pass these typed props directly into VWText / VWIcon without an intermediary JSON clone. This should noticeably reduce per-build overhead.

🧹 Nitpick comments (2)
lib/src/framework/widgets/button.dart (2)

34-36: Clarify isDisabled vs onClick interaction to avoid null action execution

isDisabled defaults to props.get('onClick') == null, but if a schema sets isDisabled to false explicitly while leaving onClick null, onPressed becomes non-null and ActionFlow.fromJson(props.get('onClick')) will receive a null payload, which may blow up at runtime depending on ActionFlow.fromJson’s implementation.

Consider tightening this coupling so the button never attempts to execute a null action, e.g. by deriving isDisabled from both flags and reusing the same onClick JSON you pass to ActionFlow:

-    final isDisabled = payload.eval<bool>(props.get('isDisabled')) ??
-        props.get('onClick') == null;
+    final onClickJson = props.get('onClick');
+    final isDisabled =
+        payload.eval<bool>(props.get('isDisabled')) ?? onClickJson == null;
...
-      onPressed: isDisabled
-          ? null
-          : () {
-              final onClick = ActionFlow.fromJson(props.get('onClick'));
+      onPressed: isDisabled
+          ? null
+          : () {
+              if (onClickJson == null) return;
+              final onClick = ActionFlow.fromJson(onClickJson);
               payload.executeAction(onClick, triggerType: 'onPressed');
             },

This keeps the override behavior while guaranteeing the callback never sees a null JSON.

Also applies to: 93-99


42-52: Color override wiring looks good; tighten icon null-handling for consistency

The new flow for propagating per‑state colors down into text and icons is a nice cleanup:

  • textColor, leadingIconColor, and trailingIconColor are derived once from defaultStyleJson / disabledStyleJson.
  • _buildContent applies textColor via setValueFor('text.textStyle.textColor', ...) only when non‑null, preserving existing text styles otherwise.
  • Icons are conditionally rendered based on *.iconData presence, and the Row always contains three children, which keeps layout stable.

Two small points to consider:

  1. Icon color override null-handling
    For text, you only override when textColor != null, but for icons you always call copyWith(color: ExprOr.fromJson<String>(leadingIconColor)) even when the color string is null. Depending on how IconProps.copyWith is implemented, that may either be fine or could inadvertently clear an existing color instead of leaving it unchanged.

    To mirror the text behavior more closely and make intent explicit:

  •  final leadingIconProps =
    
  •      (localProps['leadingIcon'] as Map<String, Object?>?)
    
  •          .maybe(IconProps.fromJson)
    
  •          ?.copyWith(color: ExprOr.fromJson<String>(leadingIconColor));
    
  •  final baseLeadingIconProps =
    
  •      (localProps['leadingIcon'] as Map<String, Object?>?)
    
  •          .maybe(IconProps.fromJson);
    
  •  final leadingIconProps = leadingIconColor != null
    
  •      ? baseLeadingIconProps
    
  •          ?.copyWith(color: ExprOr.fromJson<String>(leadingIconColor))
    
  •      : baseLeadingIconProps;
    
    ...
  •  final trailingIconProps =
    
  •      (localProps['trailingIcon'] as Map<String, Object?>?)
    
  •          .maybe(IconProps.fromJson)
    
  •          ?.copyWith(color: ExprOr.fromJson<String>(trailingIconColor));
    
  •  final baseTrailingIconProps =
    
  •      (localProps['trailingIcon'] as Map<String, Object?>?)
    
  •          .maybe(IconProps.fromJson);
    
  •  final trailingIconProps = trailingIconColor != null
    
  •      ? baseTrailingIconProps
    
  •          ?.copyWith(color: ExprOr.fromJson<String>(trailingIconColor))
    
  •      : baseTrailingIconProps;
    
    
    That keeps existing icon colors untouched when no override is supplied.
    
    
  1. Local props clone is only used for overrides, which is nice separation
    Using localProps exclusively for override mutations (text color + icon colors) and still checking props.get('*.iconData') for existence keeps behavior clear and prevents false positives when icons are absent. This part of the refactor looks solid.

Also applies to: 86-91, 105-110, 114-116, 125-155, 160-163

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2068089 and 2bcb1b6.

📒 Files selected for processing (1)
  • lib/src/framework/widgets/button.dart (2 hunks)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
lib/src/framework/widgets/button.dart (3)

37-40: Default state backgroundColor lacks null fallback.

The disabled state has a fallback color (Color(0xFFE0E0E0)), but the default state at line 40 could return null if evalColor returns null. This may cause unexpected rendering behavior.

Apply this diff to add a fallback:

 final backgroundColor = isDisabled
     ? (payload.evalColor(disabledStyleJson.getString('backgroundColor')) ??
         const Color(0xFFE0E0E0))
-    : payload.evalColor(defaultStyleJson.getString('backgroundColor'));
+    : (payload.evalColor(defaultStyleJson.getString('backgroundColor')) ??
+        Colors.blue); // or appropriate default color

76-80: Fix Size construction to avoid infinite dimensions.

Using double.infinity as a fallback for a missing dimension causes the button to expand infinitely in that direction, leading to layout overflow issues.

Apply this diff to fix:

-      fixedSize: (width != null || height != null)
-          ? WidgetStateProperty.all(
-              Size(width ?? double.infinity, height ?? double.infinity),
-            )
-          : null,
+      fixedSize: (width != null && height != null)
+          ? WidgetStateProperty.all(Size(width, height))
+          : null,

If you need to support single-dimension constraints, use minimumSize or maximumSize instead of fixedSize.


109-110: Performance concern: Avoid expensive JSON serialization for props cloning.

Using jsonDecode(jsonEncode(props.value)) for deep cloning is computationally expensive and executes on every render. This could significantly impact performance when rendering many buttons.

Consider a more efficient approach - only modify what's needed:

// Example: Clone only the text props if textColor override is needed
final textProps = as$<JsonLike>(props.value['text']).maybe(TextProps.fromJson) ?? TextProps();
final modifiedTextProps = textColor != null 
    ? textProps.copyWith(textStyle: textProps.textStyle?.copyWith(color: textColor))
    : textProps;
🧹 Nitpick comments (1)
lib/src/framework/widgets/button.dart (1)

159-166: Consider filtering null icons from Row children.

Using SizedBox.shrink() for absent icons works but adds invisible widgets to the tree. A minor optimization would be to filter them out:

 return Row(
   mainAxisSize: MainAxisSize.min,
-  children: [
-    leadingIcon ?? const SizedBox.shrink(),
-    Flexible(child: text),
-    trailingIcon ?? const SizedBox.shrink(),
-  ],
+  children: [
+    if (leadingIcon != null) leadingIcon,
+    Flexible(child: text),
+    if (trailingIcon != null) trailingIcon,
+  ],
 );

This avoids creating empty SizedBox widgets when icons are absent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bcb1b6 and 3aa119e.

📒 Files selected for processing (1)
  • lib/src/framework/widgets/button.dart (3 hunks)
🔇 Additional comments (2)
lib/src/framework/widgets/button.dart (2)

42-52: LGTM!

The per-state color derivation logic is clean and correctly distinguishes between disabled and default states for text and icon colors.


91-100: LGTM!

The button properly disables interaction when isDisabled is true by passing null to onPressed, and correctly triggers the action flow on press.

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.

2 participants