Skip to content

Add expansion with animation with gpui#2323

Merged
yujonglee merged 3 commits intomainfrom
notification-animated-gpui
Dec 15, 2025
Merged

Add expansion with animation with gpui#2323
yujonglee merged 3 commits intomainfrom
notification-animated-gpui

Conversation

@yujonglee
Copy link
Contributor

@yujonglee yujonglee commented Dec 15, 2025

Summary

Refactors the notification2 crate to support expandable notifications with smooth animations. Key changes:

  • Module reorganization: Split monolithic lib.rs into separate modules (animation.rs, constants.rs, event.rs, toast.rs)
  • Expandable content: Notifications can now include expandable content that reveals on click with animated height transitions using GPUI's cx.on_next_frame() pattern
  • Theme support: Added NotificationTheme enum with System/Light/Dark variants and a .theme() builder method to force light or dark appearance
  • Visual indicator: Shows "Expand to see more info" hint when collapsed with expandable content

Note: The System theme currently defaults to dark because cx.theme() isn't available in the notification window context. Use .theme(NotificationTheme::Light) to force light theme.

Review & Testing Checklist for Human

  • Test the animation visually: Run cargo run -p notification2 --example test_notification and verify the expand/collapse animation is smooth (200ms ease-out-quint)
  • Verify action button behavior: First click should expand the notification, second click should emit Accepted event - confirm this is the intended UX
  • Test light theme appearance: Add .theme(NotificationTheme::Light) to the example and verify the frosted glass effect looks correct
  • Check the "Expand to see more info" indicator: Should only appear when collapsed with expandable content, and should be clickable

Recommended test plan: Run the example notification, click the action button to expand, verify animation smoothness, click "Show less" to collapse, then click action button again to confirm it emits the Accepted event.

Notes

@netlify
Copy link

netlify bot commented Dec 15, 2025

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit afd4c00
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/6940269e941bda0008f1b11c
😎 Deploy Preview https://deploy-preview-2323--hyprnote.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Dec 15, 2025

Deploy Preview for hyprnote-storybook ready!

Name Link
🔨 Latest commit afd4c00
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6940269edbe379000781acff
😎 Deploy Preview https://deploy-preview-2323--hyprnote-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 15, 2025

📝 Walkthrough

Walkthrough

The notification2 crate was modularized into animation, constants, event, and toast modules. A new StatusToast UI component with builder-style configuration, expand/collapse animation, and event emission was added. Example usage was updated to use the new API and event messages.

Changes

Cohort / File(s) Summary
Crate root & re-exports
crates/notification2/src/lib.rs
Replaced inline definitions with module structure and public re-exports; now exposes NotificationEvent, NotificationTheme, and StatusToast from submodules.
Event type
crates/notification2/src/event.rs
Added pub enum NotificationEvent with variants Accepted and Dismissed.
Constants
crates/notification2/src/constants.rs
Added crate-scoped UI constants: NOTIFICATION_WIDTH, NOTIFICATION_HEIGHT_COLLAPSED, NOTIFICATION_HEIGHT_EXPANDED, NOTIFICATION_MARGIN_X, NOTIFICATION_MARGIN_Y, and ANIMATION_DURATION.
Animation utilities
crates/notification2/src/animation.rs
Added pub(crate) fn ease_out_quint(t: f32) -> f32 and pub(crate) struct AnimationState { start_time, from_height, to_height }.
Toast component
crates/notification2/src/toast.rs
New StatusToast component with builder methods (theme, subtitle, project_name, action_label, expanded_content), animation-driven expand/collapse via AnimationState, rendering (Render impl), and EventEmitter<NotificationEvent> implementation.
Example update
crates/notification2/examples/test_notification.rs
Updated StatusToast instantiation text, added action_label("Join") and expanded_content(...), and changed event log messages for Accepted and Dismissed.

Sequence Diagram

sequenceDiagram
    participant User
    participant StatusToast as StatusToast (Component)
    participant Animation as AnimationState
    participant Emitter as EventEmitter

    User->>StatusToast: Click action button
    activate StatusToast
    StatusToast->>Animation: Start expand animation
    Animation->>Animation: Interpolate height (ease_out_quint)
    StatusToast->>StatusToast: Render expanded content (clip/opacity)
    StatusToast->>Emitter: Emit Accepted
    deactivate StatusToast
    Emitter-->>User: Delivered event callback

    User->>StatusToast: Click dismiss
    activate StatusToast
    StatusToast->>Animation: Start collapse animation
    Animation->>Animation: Interpolate height
    StatusToast->>Emitter: Emit Dismissed
    deactivate StatusToast
    Emitter-->>User: Delivered event callback
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review focus:
    • crates/notification2/src/toast.rs — animation lifecycle, height interpolation, render correctness, event emission wiring.
    • crates/notification2/src/animation.rs — easing math and time handling.
    • crates/notification2/src/lib.rs — ensure re-exports and module visibility match intended public API.

Possibly related PRs

  • Different way of notif #1408 — Modifies the same notification2 crate files and may conflict with this PR's new module structure (removes/replaces notification2 in that PR).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add expansion with animation with gpui' is related to the main changes, which include adding expandable notifications with smooth animations using GPUI, but it lacks specificity and could be more descriptive.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing module reorganization, expandable content implementation, theme support, and testing guidance.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch notification-animated-gpui

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.

@devin-ai-integration
Copy link
Contributor

Code Review: crates/notification2

I reviewed the notification2 crate against the GPUI documentation. Overall the implementation is solid with good structure and proper use of GPUI patterns. Here are my findings:

Positive Aspects

  1. Good module organization - Clean separation into animation.rs, constants.rs, event.rs, and toast.rs
  2. Proper builder pattern - StatusToast::new().subtitle().project_name().expanded_content() follows GPUI conventions
  3. Correct animation approach - Using cx.on_next_frame() for frame-by-frame animation is appropriate here since you need to resize the window, not just animate element styles
  4. Proper event handling - Good use of EventEmitter<NotificationEvent> and cx.listener() for click handlers
  5. Window configuration - WindowKind::PopUp, focus: false, transparent background are all correct for notifications

Issues to Address

1. Hardcoded colors (toast.rs:183-188)

let bg = rgb(0x1e1e1e);
let border = hsla(0., 0., 1., 0.1);
let text_primary = rgb(0xf5f5f5);
let text_secondary = rgb(0x999999);
let accent = rgb(0x0a84ff);

Per GPUI best practices, these should use theme colors for proper dark/light theme support:

let colors = cx.theme().colors();
let bg = colors.elevated_surface_background;
let text_primary = colors.text;
let text_secondary = colors.text_muted;

However, if this notification is intentionally always dark (like macOS notifications), this is acceptable but should be documented.

2. Toggle button text is always "Show less ▲" (toast.rs:348)
When collapsed or collapsing, the button should show "Show more ▼". Currently it always shows "Show less ▲":

.child("Show less ▲"),

Should be:

.child(if is_expanded { "Show less ▲" } else { "Show more ▼" }),

3. No visual expand indicator in collapsed state
When there's expandable content but the notification is collapsed, users have no way to know they can expand it. The action button expands it, but this behavior isn't obvious. Consider:

  • Adding a chevron icon to the collapsed view
  • Or changing the action button label when expandable (e.g., "View Details" vs "Join")

4. Action button has dual behavior (toast.rs:285-291)
The action button either expands the notification OR emits Accepted depending on state. This could be confusing since the label doesn't change. Consider making the expand/action behaviors more explicit.

Minor Suggestions

5. Explicit type annotation (toast.rs:311)

|el: Stateful<Div>|

This works but is verbose. If type inference isn't working, it might indicate a deeper issue.

6. Consider adding hover state tracking
The GPUI docs show notifications that pause auto-dismiss on hover and show a close button. This implementation doesn't track hover state, which could be a nice enhancement.

7. Window bounds on expand
When the notification expands from 72px to 500px, if positioned near the bottom of the screen, it might extend beyond screen bounds. Consider adjusting the window position during expansion.

Questions

  • Is the dark color scheme intentional (always dark like macOS notifications) or should it respect the system theme?
  • Should there be auto-dismiss functionality after a timeout?

Overall this is a well-structured implementation. The main actionable item is the toggle button text issue (#2).

Copy link
Contributor

@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

🧹 Nitpick comments (3)
crates/notification2/src/event.rs (1)

1-4: Add standard trait derives for better usability.

The NotificationEvent enum lacks common derives that are typically useful for event types. Consider adding Debug for logging/debugging, Clone for event handling patterns, and PartialEq/Eq for testing and matching.

Apply this diff:

+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum NotificationEvent {
     Accepted,
     Dismissed,
 }
crates/notification2/src/animation.rs (2)

5-7: Consider adding input validation and documentation.

The ease_out_quint function lacks documentation and input bounds checking. While the caller in toast.rs line 77 clamps progress with .min(1.0), documenting the expected range (0.0 to 1.0) would improve clarity.

Apply this diff:

+/// Applies an ease-out quintic curve to the input.
+/// Input `t` is expected to be in the range [0.0, 1.0].
 pub(crate) fn ease_out_quint(t: f32) -> f32 {
     1.0 - (1.0 - t).powi(5)
 }

9-13: Consider adding documentation for the animation state.

The AnimationState struct would benefit from documentation explaining its purpose and usage in the height animation interpolation.

Apply this diff:

+/// Tracks the state of an ongoing height animation.
 pub(crate) struct AnimationState {
     pub start_time: Instant,
     pub from_height: Pixels,
     pub to_height: Pixels,
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b90b1cb and 14ec3f8.

📒 Files selected for processing (6)
  • crates/notification2/examples/test_notification.rs (1 hunks)
  • crates/notification2/src/animation.rs (1 hunks)
  • crates/notification2/src/constants.rs (1 hunks)
  • crates/notification2/src/event.rs (1 hunks)
  • crates/notification2/src/lib.rs (1 hunks)
  • crates/notification2/src/toast.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-26T06:09:32.213Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: crates/notification-linux/AGENTS.md:0-0
Timestamp: 2025-11-26T06:09:32.213Z
Learning: Applies to crates/notification-linux/**/*notification-linux*/**/*.{rs,toml} : Use gtk3 (not gtk4) because webkit2gtk-4.1 links to gtk3

Applied to files:

  • crates/notification2/src/lib.rs
🧬 Code graph analysis (2)
crates/notification2/src/toast.rs (1)
crates/notification2/src/animation.rs (1)
  • ease_out_quint (5-7)
crates/notification2/src/lib.rs (1)
extensions/shared/types/hypr-extension.d.ts (1)
  • toast (462-462)
⏰ 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). (3)
  • GitHub Check: desktop_ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: desktop_ci (macos, depot-macos-14)
  • GitHub Check: desktop_ci (linux, depot-ubuntu-22.04-8)
🔇 Additional comments (13)
crates/notification2/examples/test_notification.rs (2)

8-18: LGTM! Clear demonstration of the builder pattern API.

The example effectively demonstrates the new StatusToast builder pattern with project_name, subtitle, action_label, and expanded_content. The text updates make the use case more concrete and realistic.


20-33: LGTM! Event handling is correctly implemented.

The event subscription pattern correctly handles both Accepted and Dismissed events, with appropriate logging messages that reflect the updated action label.

crates/notification2/src/constants.rs (1)

5-11: LGTM! Consider whether 200ms is appropriate for the large height change.

The constants are well-defined. The height expands from 72px to 500px (~7x increase), which is a substantial change. A 200ms animation duration might feel quite fast for this transition. Consider testing whether a slightly longer duration (e.g., 250-300ms) provides a smoother user experience.

crates/notification2/src/lib.rs (1)

1-9: LGTM! Clean modularization of the crate structure.

The refactoring into separate modules (animation, constants, event, toast) improves code organization while maintaining the public API surface through re-exports. This follows standard Rust practices.

crates/notification2/src/toast.rs (9)

13-54: LGTM! Well-designed builder pattern API.

The struct and builder methods follow gpui idioms correctly, using Into<SharedString> for flexible string input and providing sensible defaults. The separation of expanded_content as optional enables both simple and complex notification use cases.


56-71: LGTM! Native-looking shadow implementation.

The two-layer shadow approach effectively creates depth and follows platform conventions for notification shadows.


73-98: LGTM! Correct animation interpolation logic.

The height calculation methods properly handle both animated and static states. The expanded_content_height correctly accounts for the collapsed height plus the 24px toggle bar, and current_content_clip_height safely handles the transition with .max(0.0).


100-129: LGTM! Proper window positioning for notification toast.

The window_options correctly positions the toast in the top-right corner with appropriate margins. The window configuration (PopUp kind, no focus, not movable, transparent background) is suitable for a non-intrusive notification.


131-177: LGTM! Robust animation loop with proper interruption handling.

The animation implementation correctly handles:

  • Interruption mid-animation by capturing current_animated_height() as the starting point (line 132)
  • Progressive window resizing during the animation (line 164-167)
  • Cleanup when animation completes (line 169-170)
  • Frame scheduling with cx.on_next_frame pattern

179-179: LGTM! Standard gpui EventEmitter implementation.


285-291: Action button behavior may be confusing when expanded.

The action button has dual behavior:

  • When collapsed with expandable content: expands the notification
  • When expanded: emits Accepted event

This means the button changes function after expansion. Users might expect it to perform the same action (expand) again or become disabled. Consider whether this is the intended UX, as it could be confusing.

If this behavior is intentional, consider adding a visual cue (e.g., changing the button label or icon) when the notification is expanded to indicate the changed behavior.


309-372: LGTM! Smooth expand/collapse animation with proper clipping.

The expanded content rendering correctly implements:

  • Conditional rendering based on expansion state or active animation
  • Opacity fade-in calculated from clip progress (lines 314-320)
  • Height clipping to create smooth expansion effect (line 356)
  • Interactive collapse toggle with clear affordance (lines 330-349)

376-391: LGTM! Clean content rendering helper.

The render_expanded_content method correctly styles the expanded content with appropriate visual separation from the header. The unwrap_or_default() is safe since this is only called when expanded_content.is_some().

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@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

🧹 Nitpick comments (1)
crates/notification2/src/toast.rs (1)

105-107: Consider extracting magic number to a constant.

The px(24.) represents the expand toggle bar height. For maintainability, consider adding this to constants.rs.

+// In constants.rs
+pub(crate) const EXPAND_TOGGLE_HEIGHT: Pixels = px(24.);

// In toast.rs
 fn expanded_content_height(&self) -> Pixels {
-    NOTIFICATION_HEIGHT_EXPANDED - NOTIFICATION_HEIGHT_COLLAPSED - px(24.)
+    NOTIFICATION_HEIGHT_EXPANDED - NOTIFICATION_HEIGHT_COLLAPSED - EXPAND_TOGGLE_HEIGHT
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14ec3f8 and afd4c00.

📒 Files selected for processing (2)
  • crates/notification2/src/lib.rs (1 hunks)
  • crates/notification2/src/toast.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-26T06:09:32.213Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: crates/notification-linux/AGENTS.md:0-0
Timestamp: 2025-11-26T06:09:32.213Z
Learning: Applies to crates/notification-linux/**/*notification-linux*/**/*.{rs,toml} : Use gtk3 (not gtk4) because webkit2gtk-4.1 links to gtk3

Applied to files:

  • crates/notification2/src/lib.rs
🧬 Code graph analysis (2)
crates/notification2/src/lib.rs (1)
extensions/shared/types/hypr-extension.d.ts (1)
  • toast (462-462)
crates/notification2/src/toast.rs (1)
crates/notification2/src/animation.rs (1)
  • ease_out_quint (5-7)
⏰ 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). (6)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: desktop_ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: desktop_ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: desktop_ci (macos, depot-macos-14)
🔇 Additional comments (8)
crates/notification2/src/lib.rs (1)

1-9: Clean module organization.

The refactoring from inline definitions to a modular structure is well-executed. The public API surface is explicit with re-exports for NotificationEvent, NotificationTheme, and StatusToast, while keeping animation and constants as internal implementation details.

crates/notification2/src/toast.rs (7)

1-19: Imports and theme enum look good.

The NotificationTheme enum is well-designed with appropriate derives. Using System as the default is a sensible choice for respecting user preferences.


21-69: Well-designed builder API.

The builder pattern with impl Into<SharedString> provides a flexible and ergonomic API. Internal state (is_expanded, animation) is correctly kept private.


115-144: Window options are well-configured for a notification popup.

The positioning logic correctly places the notification at the top-right corner. The transparent background and popup window kind are appropriate for this use case.


146-191: Solid animation implementation.

The animation state machine is correctly structured with proper frame scheduling. The window resizes smoothly during animation, and the loop correctly terminates when complete. Good use of cx.on_next_frame() for frame-perfect animation.


198-201: System theme always resolves to dark mode.

The current implementation treats NotificationTheme::System the same as Dark. If the intent is to respect the actual system appearance, this would need to query the system's dark/light mode setting.

Is this intentional, or should System detect the actual OS appearance? If detection is needed, gpui may provide an API to query this.


319-325: Action button behavior is clear.

The click handler correctly handles the two-stage interaction: first expand (if content exists), then accept. This provides a natural flow for users to preview content before accepting.


434-448: Clean content rendering helper.

The render_expanded_content method is well-structured and uses safe defaults with unwrap_or_default().

@yujonglee yujonglee merged commit 7bc51b0 into main Dec 15, 2025
16 checks passed
@yujonglee yujonglee deleted the notification-animated-gpui branch December 15, 2025 15:23
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.

1 participant