diff --git a/src/home/editing_pane.rs b/src/home/editing_pane.rs index ae89492b0..0816aba5b 100644 --- a/src/home/editing_pane.rs +++ b/src/home/editing_pane.rs @@ -21,16 +21,25 @@ script_mod! { use mod.widgets.* - mod.widgets.EditingContent = View { + mod.widgets.EditingContent = RoundedView { width: Fill, - height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} - align: Align{x: 0.5, y: 1.0}, // centered horizontally, bottom-aligned + height: Fit, padding: Inset{ left: 20, right: 20, top: 10, bottom: 10 } - margin: Inset{top: 2} spacing: 10, flow: Down, - show_bg: false // don't cover up the RoomInputBar + // this must match the RoomInputBar exactly such that it overlaps atop it. + margin: Inset{left: -4, right: -4, bottom: -4 } + show_bg: true, + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 5.0 + border_color: (COLOR_SECONDARY) + border_size: 2.0 + // shadow_color: #0006 + // shadow_radius: 0.0 + // shadow_offset: vec2(0.0,0.0) + } View { width: Fill, height: Fit @@ -83,34 +92,31 @@ script_mod! { mod.widgets.EditingPane = #(EditingPane::register_widget(vm)) { + ..mod.widgets.RoundedView + visible: false, width: Fill, height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} align: Align{x: 0.5, y: 1.0} - // TODO: FIXME: this is a hack to make the editing pane - // able to slide out of the bottom of the screen. - // (Waiting on a Makepad-level fix for this.) - margin: Inset{top: 1000} editing_content := mod.widgets.EditingContent { } + + slide: 1.0, animator: Animator{ panel: { default: @hide show: AnimatorState{ redraw: true, - from: {all: Forward {duration: 0.8}} + from: {all: Forward {duration: 0.5}} ease: ExpDecay {d1: 0.80, d2: 0.97} - apply: { margin: Inset{top: 0} } + apply: { slide: 0.0 } } hide: AnimatorState{ redraw: true, - from: {all: Forward {duration: 0.8}} + from: {all: Forward {duration: 0.5}} ease: ExpDecay {d1: 0.80, d2: 0.97} - // TODO: FIXME: this is a hack to make the editing pane - // able to slide out of the bottom of the screen. - // (Waiting on a Makepad-level fix for this.) - apply: { margin: Inset{top: 1000} } + apply: { slide: 1.0 } } } } @@ -120,7 +126,9 @@ script_mod! { /// Action emitted by the EditingPane widget. #[derive(Clone, Default, Debug)] pub enum EditingPaneAction { - /// The editing pane has been closed/hidden. + /// The editing pane's hide animation has started. + HideAnimationStarted, + /// The editing pane has been fully closed/hidden. Hidden, #[default] None, @@ -145,42 +153,57 @@ pub struct EditingPane { #[source] source: ScriptObjectRef, #[deref] view: View, #[apply_default] animator: Animator, + #[live] slide: f32, #[rust] info: Option, #[rust] is_animating_out: bool, + #[rust] last_content_height: f64, + /// A pending next-frame request used to force a parent relayout + /// after the hide animation completes. + #[rust] next_frame: NextFrame, } impl Widget for EditingPane { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + // Handle the next-frame event scheduled after hide animation completes. + // This forces a full redraw cycle so the parent relayouts properly. + if self.next_frame.is_event(event).is_some() { + cx.redraw_all(); + } + self.view.handle_event(cx, event, scope); if !self.visible { return; } let animator_action = self.animator_handle_event(cx, event); if animator_action.must_redraw() { - self.redraw(cx); + // During hide, redraw the entire UI so the parent RoomInputBar + // can animate the input_bar height in its draw_walk. + // During show, only this widget needs to redraw. + if self.is_animating_out { + cx.redraw_all(); + } else { + self.redraw(cx); + } } - // If the animator is in the `hide` state and has finished animating out, - // that means it has fully animated off-screen and can be set to invisible. - if self.animator_in_state(cx, ids!(panel.hide)) { - match ( - self.is_animating_out, - matches!(animator_action, AnimatorAction::Animating { .. }), - ) { - (true, false) => { - self.visible = false; - self.info = None; - cx.widget_action(self.widget_uid(), EditingPaneAction::Hidden); - cx.revert_key_focus(); - self.redraw(cx); - return; - }, - (false, true) => { - self.is_animating_out = true; - return; - }, - _ => {}, + // If we started animating the hide, check if the track has finished. + // `is_track_animating` returns false once the track has fully completed, + // even on the same frame that returned the last `Animating` action. + if self.is_animating_out { + if !self.animator.is_track_animating(id!(panel)) { + self.visible = false; + self.is_animating_out = false; + self.info = None; + cx.widget_action(self.widget_uid(), EditingPaneAction::Hidden); + cx.revert_key_focus(); + self.redraw(cx); + self.next_frame = cx.new_next_frame(); + return; } + } else if self.animator_in_state(cx, ids!(panel.hide)) + && matches!(animator_action, AnimatorAction::Animating { .. }) + { + self.is_animating_out = true; } if let Event::Actions(actions) = event { @@ -195,6 +218,7 @@ impl Widget for EditingPane { || edit_text_input.escaped(actions) { self.animator_play(cx, ids!(panel.hide)); + cx.widget_action(self.widget_uid(), EditingPaneAction::HideAnimationStarted); self.redraw(cx); return; } @@ -283,6 +307,7 @@ impl Widget for EditingPane { None, ); self.animator_play(cx, ids!(panel.hide)); + cx.widget_action(self.widget_uid(), EditingPaneAction::HideAnimationStarted); self.redraw(cx); return; }, @@ -367,11 +392,53 @@ impl Widget for EditingPane { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, mut walk: Walk) -> DrawStep { if self.info.is_none() { self.visible = false; }; - self.view.draw_walk(cx, scope, walk) + + // Animate both the layout height and content position simultaneously: + // 1. walk.height grows from 0 to ch (and shrinks back during hide), + // so the RoomInputBar border grows/shrinks smoothly. + // 2. Balanced margins on editing_content slide it within the pane: + // margin.top pushes content below the clip boundary, + // margin.bottom compensates so the Fit height stays constant. + // The pane's show_bg provides the clipping. + let ch = self.last_content_height; + if self.slide > 0.001 { + let offset = if ch > 0.0 { + ch * self.slide as f64 + } else { + 10000.0 + }; + if let Some(mut ec) = self.view(cx, ids!(editing_content)).borrow_mut() { + ec.walk.margin.top = offset; + ec.walk.margin.bottom = -offset; + } + // Animate the layout height alongside the content slide, + // so the RoomInputBar border grows/shrinks smoothly. + if ch > 0.0 { + walk.height = Size::Fixed((ch * (1.0 - self.slide as f64)).max(0.0)); + } else { + walk.height = Size::Fixed(0.0); + } + } else { + // Fully shown or not animating: reset margins. + if let Some(mut ec) = self.view(cx, ids!(editing_content)).borrow_mut() { + ec.walk.margin.top = 0.0; + ec.walk.margin.bottom = 0.0; + } + } + + let step = self.view.draw_walk(cx, scope, walk); + + // Read area rect AFTER drawing to capture this frame's layout. + let ec_height = self.view(cx, ids!(editing_content)).area().rect(cx).size.y; + if ec_height > 0.0 { + self.last_content_height = ec_height; + } + + step } } @@ -402,6 +469,7 @@ impl EditingPane { match edit_result { Ok(()) => { self.animator_play(cx, ids!(panel.hide)); + cx.widget_action(self.widget_uid(), EditingPaneAction::HideAnimationStarted); }, Err(e) => { enqueue_popup_notification( @@ -451,6 +519,7 @@ impl EditingPane { }); self.visible = true; + self.is_animating_out = false; self.button(cx, ids!(accept_button)).reset_hover(cx); self.button(cx, ids!(cancel_button)).reset_hover(cx); self.animator_play(cx, ids!(panel.show)); @@ -496,6 +565,7 @@ impl EditingPane { timeline_kind, }); self.visible = true; + self.is_animating_out = false; self.button(cx, ids!(accept_button)).reset_hover(cx); self.button(cx, ids!(cancel_button)).reset_hover(cx); self.animator_play(cx, ids!(panel.show)); @@ -517,6 +587,11 @@ impl EditingPaneRef { inner.is_currently_shown(cx) } + /// Returns the current slide value (0.0 = fully shown, 1.0 = fully hidden). + pub fn slide(&self) -> f32 { + self.borrow().map_or(1.0, |inner| inner.slide) + } + /// See [`EditingPane::handle_edit_result()`]. pub fn handle_edit_result( &self, @@ -537,6 +612,14 @@ impl EditingPaneRef { ) } + /// Returns whether this `EditingPane`'s hide animation started in the given actions. + pub fn was_hide_animation_started(&self, actions: &Actions) -> bool { + matches!( + actions.find_widget_action(self.widget_uid()).cast_ref(), + EditingPaneAction::HideAnimationStarted, + ) + } + /// See [`EditingPane::show()`]. pub fn show( &self, @@ -575,7 +658,14 @@ impl EditingPaneRef { inner.animator_cut(cx, ids!(panel.hide)); inner.is_animating_out = false; inner.info = None; - inner.redraw(cx); + // Reset editing_content margins in case we interrupted an animation. + if let Some(mut ec) = inner.view(cx, ids!(editing_content)).borrow_mut() { + ec.walk.margin.top = 0.0; + ec.walk.margin.bottom = 0.0; + } + // Redraw all so the parent RoomInputBar restores the input_bar + // height (its draw_walk reads the slide value, which is now 1.0). + cx.redraw_all(); } } diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 93b8d4a9d..68a9f66c3 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -42,7 +42,6 @@ script_mod! { // This only works if the border_color is the same as its parents, // which is currently `COLOR_SECONDARY`. margin: Inset{left: -4, right: -4, bottom: -4 } - show_bg: true, draw_bg +: { color: (COLOR_PRIMARY) @@ -169,6 +168,9 @@ pub struct RoomInputBar { #[rust] was_replying_preview_visible: bool, /// Info about the message event that the user is currently replying to, if any. #[rust] replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, + /// Cached natural Fit height of the input_bar, used as the animation + /// target when the editing pane is being hidden. + #[rust] input_bar_natural_height: f64, } impl Widget for RoomInputBar { @@ -207,6 +209,34 @@ impl Widget for RoomInputBar { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + // Shrink the input_bar's height as the editing pane slides in, + // and grow it back as the editing pane slides out. + // slide=1.0 → editing pane hidden → input_bar at full Fit height. + // slide=0.0 → editing pane shown → input_bar at zero height. + let slide = self.editing_pane(cx, ids!(editing_pane)).slide(); + let input_bar = self.view.view(cx, ids!(input_bar)); + + // Remap slide through a steeper curve so the input_bar reaches + // its full target height before the ExpDecay tail. + let remapped = (slide as f64 * 1.25).min(1.0); + if remapped >= 1.0 { + // Input_bar has reached its full natural height: switch to Fit + // so it can respond to content changes normally. + // Update the cached height for future animations. + let h = input_bar.area().rect(cx).size.y; + if h > 0.0 { + self.input_bar_natural_height = h; + } + if let Some(mut inner) = input_bar.borrow_mut() { + inner.walk.height = Size::fit(); + } + } else { + let target = self.input_bar_natural_height; + if let Some(mut inner) = input_bar.borrow_mut() { + inner.walk.height = Size::Fixed((target * remapped).max(0.0)); + } + } + self.view.draw_walk(cx, scope, walk) } } @@ -359,7 +389,7 @@ impl RoomInputBar { } } - // If the EditingPane has been hidden, handle that. + // When the hide animation fully completes, restore the replying preview. if self.view.editing_pane(cx, ids!(editing_pane)).was_hidden(actions) { self.on_editing_pane_hidden(cx); } @@ -434,13 +464,15 @@ impl RoomInputBar { behavior: ShowEditingPaneBehavior, timeline_kind: TimelineKind, ) { - // We must hide the input_bar while the editing pane is shown, - // otherwise a very-tall inputted message might show up underneath a shorter editing pane. - self.view.view(cx, ids!(input_bar)).set_visible(cx, false); + // Cache the input_bar's natural height before the animation shrinks it. + let input_bar_height = self.view.view(cx, ids!(input_bar)).area().rect(cx).size.y; + if input_bar_height > 0.0 { + self.input_bar_natural_height = input_bar_height; + } - // Similarly, we must hide the replying preview and location preview, - // since those are not relevant to editing an existing message, - // so keeping them visible might confuse the user. + // Hide the replying preview and location preview while the editing + // pane is shown. The input_bar is not hidden; instead it is slid out + // of view in draw_walk using the EditingPane's slide value. let replying_preview = self.view.view(cx, ids!(replying_preview)); self.was_replying_preview_visible = replying_preview.visible(); replying_preview.set_visible(cx, false); @@ -461,10 +493,7 @@ impl RoomInputBar { /// This should be invoked after the EditingPane has been fully hidden. fn on_editing_pane_hidden(&mut self, cx: &mut Cx) { - // In `show_editing_pane()` above, we hid the input_bar while the editing pane - // was being shown, so here we need to make it visible again. - // Same goes for the replying_preview, if it was previously shown. - self.view.view(cx, ids!(input_bar)).set_visible(cx, true); + // Restore the replying_preview. if self.was_replying_preview_visible && self.replying_to.is_some() { self.view.view(cx, ids!(replying_preview)).set_visible(cx, true); } @@ -489,9 +518,7 @@ impl RoomInputBar { input_bar.set_visible(cx, false); } else { tombstone_footer.hide(cx); - if !self.editing_pane(cx, ids!(editing_pane)).is_currently_shown(cx) { - input_bar.set_visible(cx, true); - } + input_bar.set_visible(cx, true); } }