1use crate::{
2 AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings,
3 pane_group::element::pane_axis,
4 workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
5};
6use anyhow::Result;
7use call::{ActiveCall, ParticipantLocation};
8use collections::HashMap;
9use gpui::{
10 Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
11 Point, StyleRefinement, WeakEntity, Window, point, size,
12};
13use parking_lot::Mutex;
14use project::Project;
15use schemars::JsonSchema;
16use serde::Deserialize;
17use settings::Settings;
18use std::sync::Arc;
19use ui::prelude::*;
20
21pub const HANDLE_HITBOX_SIZE: f32 = 4.0;
22const HORIZONTAL_MIN_SIZE: f32 = 80.;
23const VERTICAL_MIN_SIZE: f32 = 100.;
24
25/// One or many panes, arranged in a horizontal or vertical axis due to a split.
26/// Panes have all their tabs and capabilities preserved, and can be split again or resized.
27/// Single-pane group is a regular pane.
28#[derive(Clone)]
29pub struct PaneGroup {
30 pub root: Member,
31 pub is_center: bool,
32}
33
34pub struct PaneRenderResult {
35 pub element: gpui::AnyElement,
36 pub contains_active_pane: bool,
37}
38
39impl PaneGroup {
40 pub fn with_root(root: Member) -> Self {
41 Self {
42 root,
43 is_center: false,
44 }
45 }
46
47 pub fn new(pane: Entity<Pane>) -> Self {
48 Self {
49 root: Member::Pane(pane),
50 is_center: false,
51 }
52 }
53
54 pub fn set_is_center(&mut self, is_center: bool) {
55 self.is_center = is_center;
56 }
57
58 pub fn split(
59 &mut self,
60 old_pane: &Entity<Pane>,
61 new_pane: &Entity<Pane>,
62 direction: SplitDirection,
63 cx: &mut App,
64 ) -> Result<()> {
65 let result = match &mut self.root {
66 Member::Pane(pane) => {
67 if pane == old_pane {
68 self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
69 Ok(())
70 } else {
71 anyhow::bail!("Pane not found");
72 }
73 }
74 Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
75 };
76 if result.is_ok() {
77 self.mark_positions(cx);
78 }
79 result
80 }
81
82 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
83 match &self.root {
84 Member::Pane(_) => None,
85 Member::Axis(axis) => axis.bounding_box_for_pane(pane),
86 }
87 }
88
89 pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&Entity<Pane>> {
90 match &self.root {
91 Member::Pane(pane) => Some(pane),
92 Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
93 }
94 }
95
96 /// Moves active pane to span the entire border in the given direction,
97 /// similar to Vim ctrl+w shift-[hjkl] motion.
98 ///
99 /// Returns:
100 /// - Ok(true) if it found and moved a pane
101 /// - Ok(false) if it found but did not move the pane
102 /// - Err(_) if it did not find the pane
103 pub fn move_to_border(
104 &mut self,
105 active_pane: &Entity<Pane>,
106 direction: SplitDirection,
107 cx: &mut App,
108 ) -> Result<bool> {
109 if let Some(pane) = self.find_pane_at_border(direction)
110 && pane == active_pane
111 {
112 return Ok(false);
113 }
114
115 if !self.remove_internal(active_pane)? {
116 return Ok(false);
117 }
118
119 if let Member::Axis(root) = &mut self.root
120 && direction.axis() == root.axis
121 {
122 let idx = if direction.increasing() {
123 root.members.len()
124 } else {
125 0
126 };
127 root.insert_pane(idx, active_pane);
128 self.mark_positions(cx);
129 return Ok(true);
130 }
131
132 let members = if direction.increasing() {
133 vec![self.root.clone(), Member::Pane(active_pane.clone())]
134 } else {
135 vec![Member::Pane(active_pane.clone()), self.root.clone()]
136 };
137 self.root = Member::Axis(PaneAxis::new(direction.axis(), members));
138 self.mark_positions(cx);
139 Ok(true)
140 }
141
142 fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity<Pane>> {
143 match &self.root {
144 Member::Pane(pane) => Some(pane),
145 Member::Axis(axis) => axis.find_pane_at_border(direction),
146 }
147 }
148
149 /// Returns:
150 /// - Ok(true) if it found and removed a pane
151 /// - Ok(false) if it found but did not remove the pane
152 /// - Err(_) if it did not find the pane
153 pub fn remove(&mut self, pane: &Entity<Pane>, cx: &mut App) -> Result<bool> {
154 let result = self.remove_internal(pane);
155 if let Ok(true) = result {
156 self.mark_positions(cx);
157 }
158 result
159 }
160
161 fn remove_internal(&mut self, pane: &Entity<Pane>) -> Result<bool> {
162 match &mut self.root {
163 Member::Pane(_) => Ok(false),
164 Member::Axis(axis) => {
165 if let Some(last_pane) = axis.remove(pane)? {
166 self.root = last_pane;
167 }
168 Ok(true)
169 }
170 }
171 }
172
173 pub fn resize(
174 &mut self,
175 pane: &Entity<Pane>,
176 direction: Axis,
177 amount: Pixels,
178 bounds: &Bounds<Pixels>,
179 cx: &mut App,
180 ) {
181 match &mut self.root {
182 Member::Pane(_) => {}
183 Member::Axis(axis) => {
184 let _ = axis.resize(pane, direction, amount, bounds);
185 }
186 };
187 self.mark_positions(cx);
188 }
189
190 pub fn reset_pane_sizes(&mut self, cx: &mut App) {
191 match &mut self.root {
192 Member::Pane(_) => {}
193 Member::Axis(axis) => {
194 let _ = axis.reset_pane_sizes();
195 }
196 };
197 self.mark_positions(cx);
198 }
199
200 pub fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>, cx: &mut App) {
201 match &mut self.root {
202 Member::Pane(_) => {}
203 Member::Axis(axis) => axis.swap(from, to),
204 };
205 self.mark_positions(cx);
206 }
207
208 pub fn mark_positions(&mut self, cx: &mut App) {
209 self.root.mark_positions(self.is_center, true, true, cx);
210 }
211
212 pub fn render(
213 &self,
214 zoomed: Option<&AnyWeakView>,
215 render_cx: &dyn PaneLeaderDecorator,
216 window: &mut Window,
217 cx: &mut App,
218 ) -> impl IntoElement {
219 self.root.render(0, zoomed, render_cx, window, cx).element
220 }
221
222 pub fn panes(&self) -> Vec<&Entity<Pane>> {
223 let mut panes = Vec::new();
224 self.root.collect_panes(&mut panes);
225 panes
226 }
227
228 pub fn first_pane(&self) -> Entity<Pane> {
229 self.root.first_pane()
230 }
231
232 pub fn last_pane(&self) -> Entity<Pane> {
233 self.root.last_pane()
234 }
235
236 pub fn find_pane_in_direction(
237 &mut self,
238 active_pane: &Entity<Pane>,
239 direction: SplitDirection,
240 cx: &App,
241 ) -> Option<&Entity<Pane>> {
242 let bounding_box = self.bounding_box_for_pane(active_pane)?;
243 let cursor = active_pane.read(cx).pixel_position_of_cursor(cx);
244 let center = match cursor {
245 Some(cursor) if bounding_box.contains(&cursor) => cursor,
246 _ => bounding_box.center(),
247 };
248
249 let distance_to_next = crate::HANDLE_HITBOX_SIZE;
250
251 let target = match direction {
252 SplitDirection::Left => {
253 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
254 }
255 SplitDirection::Right => {
256 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
257 }
258 SplitDirection::Up => {
259 Point::new(center.x, bounding_box.top() - distance_to_next.into())
260 }
261 SplitDirection::Down => {
262 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
263 }
264 };
265 self.pane_at_pixel_position(target)
266 }
267
268 pub fn invert_axies(&mut self, cx: &mut App) {
269 self.root.invert_pane_axies();
270 self.mark_positions(cx);
271 }
272}
273
274#[derive(Debug, Clone)]
275pub enum Member {
276 Axis(PaneAxis),
277 Pane(Entity<Pane>),
278}
279
280impl Member {
281 pub fn mark_positions(
282 &mut self,
283 in_center_group: bool,
284 is_upper_left: bool,
285 is_upper_right: bool,
286 cx: &mut App,
287 ) {
288 match self {
289 Member::Axis(pane_axis) => {
290 let len = pane_axis.members.len();
291 for (idx, member) in pane_axis.members.iter_mut().enumerate() {
292 let member_upper_left = match pane_axis.axis {
293 Axis::Vertical => is_upper_left && idx == 0,
294 Axis::Horizontal => is_upper_left && idx == 0,
295 };
296 let member_upper_right = match pane_axis.axis {
297 Axis::Vertical => is_upper_right && idx == 0,
298 Axis::Horizontal => is_upper_right && idx == len - 1,
299 };
300 member.mark_positions(
301 in_center_group,
302 member_upper_left,
303 member_upper_right,
304 cx,
305 );
306 }
307 }
308 Member::Pane(entity) => entity.update(cx, |pane, _| {
309 pane.in_center_group = in_center_group;
310 pane.is_upper_left = is_upper_left;
311 pane.is_upper_right = is_upper_right;
312 }),
313 }
314 }
315}
316
317#[derive(Clone, Copy)]
318pub struct PaneRenderContext<'a> {
319 pub project: &'a Entity<Project>,
320 pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
321 pub active_call: Option<&'a Entity<ActiveCall>>,
322 pub active_pane: &'a Entity<Pane>,
323 pub app_state: &'a Arc<AppState>,
324 pub workspace: &'a WeakEntity<Workspace>,
325}
326
327#[derive(Default)]
328pub struct LeaderDecoration {
329 border: Option<Hsla>,
330 status_box: Option<AnyElement>,
331}
332
333pub trait PaneLeaderDecorator {
334 fn decorate(&self, pane: &Entity<Pane>, cx: &App) -> LeaderDecoration;
335 fn active_pane(&self) -> &Entity<Pane>;
336 fn workspace(&self) -> &WeakEntity<Workspace>;
337}
338
339pub struct ActivePaneDecorator<'a> {
340 active_pane: &'a Entity<Pane>,
341 workspace: &'a WeakEntity<Workspace>,
342}
343
344impl<'a> ActivePaneDecorator<'a> {
345 pub fn new(active_pane: &'a Entity<Pane>, workspace: &'a WeakEntity<Workspace>) -> Self {
346 Self {
347 active_pane,
348 workspace,
349 }
350 }
351}
352
353impl PaneLeaderDecorator for ActivePaneDecorator<'_> {
354 fn decorate(&self, _: &Entity<Pane>, _: &App) -> LeaderDecoration {
355 LeaderDecoration::default()
356 }
357 fn active_pane(&self) -> &Entity<Pane> {
358 self.active_pane
359 }
360
361 fn workspace(&self) -> &WeakEntity<Workspace> {
362 self.workspace
363 }
364}
365
366impl PaneLeaderDecorator for PaneRenderContext<'_> {
367 fn decorate(&self, pane: &Entity<Pane>, cx: &App) -> LeaderDecoration {
368 let follower_state = self.follower_states.iter().find_map(|(leader_id, state)| {
369 if state.center_pane == *pane {
370 Some((*leader_id, state))
371 } else {
372 None
373 }
374 });
375 let Some((leader_id, follower_state)) = follower_state else {
376 return LeaderDecoration::default();
377 };
378
379 let mut leader_color;
380 let status_box;
381 match leader_id {
382 CollaboratorId::PeerId(peer_id) => {
383 let Some(leader) = self.active_call.as_ref().and_then(|call| {
384 let room = call.read(cx).room()?.read(cx);
385 room.remote_participant_for_peer_id(peer_id)
386 }) else {
387 return LeaderDecoration::default();
388 };
389
390 let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| {
391 !follower_state
392 .items_by_leader_view_id
393 .contains_key(&view_id)
394 });
395
396 let mut leader_join_data = None;
397 let leader_status_box = match leader.location {
398 ParticipantLocation::SharedProject {
399 project_id: leader_project_id,
400 } => {
401 if Some(leader_project_id) == self.project.read(cx).remote_id() {
402 is_in_unshared_view.then(|| {
403 Label::new(format!(
404 "{} is in an unshared pane",
405 leader.user.github_login
406 ))
407 })
408 } else {
409 leader_join_data = Some((leader_project_id, leader.user.id));
410 Some(Label::new(format!(
411 "Follow {} to their active project",
412 leader.user.github_login,
413 )))
414 }
415 }
416 ParticipantLocation::UnsharedProject => Some(Label::new(format!(
417 "{} is viewing an unshared Zed project",
418 leader.user.github_login
419 ))),
420 ParticipantLocation::External => Some(Label::new(format!(
421 "{} is viewing a window outside of Zed",
422 leader.user.github_login
423 ))),
424 };
425 status_box = leader_status_box.map(|status| {
426 div()
427 .absolute()
428 .w_96()
429 .bottom_3()
430 .right_3()
431 .elevation_2(cx)
432 .p_1()
433 .child(status)
434 .when_some(
435 leader_join_data,
436 |this, (leader_project_id, leader_user_id)| {
437 let app_state = self.app_state.clone();
438 this.cursor_pointer().on_mouse_down(
439 MouseButton::Left,
440 move |_, _, cx| {
441 crate::join_in_room_project(
442 leader_project_id,
443 leader_user_id,
444 app_state.clone(),
445 cx,
446 )
447 .detach_and_log_err(cx);
448 },
449 )
450 },
451 )
452 .into_any_element()
453 });
454 leader_color = cx
455 .theme()
456 .players()
457 .color_for_participant(leader.participant_index.0)
458 .cursor;
459 }
460 CollaboratorId::Agent => {
461 status_box = None;
462 leader_color = cx.theme().players().agent().cursor;
463 }
464 }
465
466 let is_in_panel = follower_state.dock_pane.is_some();
467 if is_in_panel {
468 leader_color.fade_out(0.75);
469 } else {
470 leader_color.fade_out(0.3);
471 }
472
473 LeaderDecoration {
474 status_box,
475 border: Some(leader_color),
476 }
477 }
478
479 fn active_pane(&self) -> &Entity<Pane> {
480 self.active_pane
481 }
482
483 fn workspace(&self) -> &WeakEntity<Workspace> {
484 self.workspace
485 }
486}
487
488impl Member {
489 fn new_axis(old_pane: Entity<Pane>, new_pane: Entity<Pane>, direction: SplitDirection) -> Self {
490 use Axis::*;
491 use SplitDirection::*;
492
493 let axis = match direction {
494 Up | Down => Vertical,
495 Left | Right => Horizontal,
496 };
497
498 let members = match direction {
499 Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
500 Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
501 };
502
503 Member::Axis(PaneAxis::new(axis, members))
504 }
505
506 fn first_pane(&self) -> Entity<Pane> {
507 match self {
508 Member::Axis(axis) => axis.members[0].first_pane(),
509 Member::Pane(pane) => pane.clone(),
510 }
511 }
512
513 fn last_pane(&self) -> Entity<Pane> {
514 match self {
515 Member::Axis(axis) => axis.members.last().unwrap().last_pane(),
516 Member::Pane(pane) => pane.clone(),
517 }
518 }
519
520 pub fn render(
521 &self,
522 basis: usize,
523 zoomed: Option<&AnyWeakView>,
524 render_cx: &dyn PaneLeaderDecorator,
525 window: &mut Window,
526 cx: &mut App,
527 ) -> PaneRenderResult {
528 match self {
529 Member::Pane(pane) => {
530 if zoomed == Some(&pane.downgrade().into()) {
531 return PaneRenderResult {
532 element: div().into_any(),
533 contains_active_pane: false,
534 };
535 }
536
537 let decoration = render_cx.decorate(pane, cx);
538 let is_active = pane == render_cx.active_pane();
539
540 PaneRenderResult {
541 element: div()
542 .relative()
543 .flex_1()
544 .size_full()
545 .child(
546 AnyView::from(pane.clone())
547 .cached(StyleRefinement::default().v_flex().size_full()),
548 )
549 .when_some(decoration.border, |this, color| {
550 this.child(
551 div()
552 .absolute()
553 .size_full()
554 .left_0()
555 .top_0()
556 .border_2()
557 .border_color(color),
558 )
559 })
560 .children(decoration.status_box)
561 .into_any(),
562 contains_active_pane: is_active,
563 }
564 }
565 Member::Axis(axis) => axis.render(basis + 1, zoomed, render_cx, window, cx),
566 }
567 }
568
569 fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a Entity<Pane>>) {
570 match self {
571 Member::Axis(axis) => {
572 for member in &axis.members {
573 member.collect_panes(panes);
574 }
575 }
576 Member::Pane(pane) => panes.push(pane),
577 }
578 }
579
580 fn invert_pane_axies(&mut self) {
581 match self {
582 Self::Axis(axis) => {
583 axis.axis = axis.axis.invert();
584 for member in axis.members.iter_mut() {
585 member.invert_pane_axies();
586 }
587 }
588 Self::Pane(_) => {}
589 }
590 }
591}
592
593#[derive(Debug, Clone)]
594pub struct PaneAxis {
595 pub axis: Axis,
596 pub members: Vec<Member>,
597 pub flexes: Arc<Mutex<Vec<f32>>>,
598 pub bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
599}
600
601impl PaneAxis {
602 pub fn new(axis: Axis, members: Vec<Member>) -> Self {
603 let flexes = Arc::new(Mutex::new(vec![1.; members.len()]));
604 let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
605 Self {
606 axis,
607 members,
608 flexes,
609 bounding_boxes,
610 }
611 }
612
613 pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> Self {
614 let mut flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
615 if flexes.len() != members.len()
616 || (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() >= 0.001
617 {
618 flexes = vec![1.; members.len()];
619 }
620
621 let flexes = Arc::new(Mutex::new(flexes));
622 let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
623 Self {
624 axis,
625 members,
626 flexes,
627 bounding_boxes,
628 }
629 }
630
631 fn split(
632 &mut self,
633 old_pane: &Entity<Pane>,
634 new_pane: &Entity<Pane>,
635 direction: SplitDirection,
636 ) -> Result<()> {
637 for (mut idx, member) in self.members.iter_mut().enumerate() {
638 match member {
639 Member::Axis(axis) => {
640 if axis.split(old_pane, new_pane, direction).is_ok() {
641 return Ok(());
642 }
643 }
644 Member::Pane(pane) => {
645 if pane == old_pane {
646 if direction.axis() == self.axis {
647 if direction.increasing() {
648 idx += 1;
649 }
650 self.insert_pane(idx, new_pane);
651 } else {
652 *member =
653 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
654 }
655 return Ok(());
656 }
657 }
658 }
659 }
660 anyhow::bail!("Pane not found");
661 }
662
663 fn insert_pane(&mut self, idx: usize, new_pane: &Entity<Pane>) {
664 self.members.insert(idx, Member::Pane(new_pane.clone()));
665 *self.flexes.lock() = vec![1.; self.members.len()];
666 }
667
668 fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity<Pane>> {
669 if self.axis != direction.axis() {
670 return None;
671 }
672 let member = if direction.increasing() {
673 self.members.last()
674 } else {
675 self.members.first()
676 };
677 member.and_then(|e| match e {
678 Member::Pane(pane) => Some(pane),
679 Member::Axis(_) => None,
680 })
681 }
682
683 fn remove(&mut self, pane_to_remove: &Entity<Pane>) -> Result<Option<Member>> {
684 let mut found_pane = false;
685 let mut remove_member = None;
686 for (idx, member) in self.members.iter_mut().enumerate() {
687 match member {
688 Member::Axis(axis) => {
689 if let Ok(last_pane) = axis.remove(pane_to_remove) {
690 if let Some(last_pane) = last_pane {
691 *member = last_pane;
692 }
693 found_pane = true;
694 break;
695 }
696 }
697 Member::Pane(pane) => {
698 if pane == pane_to_remove {
699 found_pane = true;
700 remove_member = Some(idx);
701 break;
702 }
703 }
704 }
705 }
706
707 if found_pane {
708 if let Some(idx) = remove_member {
709 self.members.remove(idx);
710 *self.flexes.lock() = vec![1.; self.members.len()];
711 }
712
713 if self.members.len() == 1 {
714 let result = self.members.pop();
715 *self.flexes.lock() = vec![1.; self.members.len()];
716 Ok(result)
717 } else {
718 Ok(None)
719 }
720 } else {
721 anyhow::bail!("Pane not found");
722 }
723 }
724
725 fn reset_pane_sizes(&self) {
726 *self.flexes.lock() = vec![1.; self.members.len()];
727 for member in self.members.iter() {
728 if let Member::Axis(axis) = member {
729 axis.reset_pane_sizes();
730 }
731 }
732 }
733
734 fn resize(
735 &mut self,
736 pane: &Entity<Pane>,
737 axis: Axis,
738 amount: Pixels,
739 bounds: &Bounds<Pixels>,
740 ) -> Option<bool> {
741 let container_size = self
742 .bounding_boxes
743 .lock()
744 .iter()
745 .filter_map(|e| *e)
746 .reduce(|acc, e| acc.union(&e))
747 .unwrap_or(*bounds)
748 .size;
749
750 let found_pane = self
751 .members
752 .iter()
753 .any(|member| matches!(member, Member::Pane(p) if p == pane));
754
755 if found_pane && self.axis != axis {
756 return Some(false); // pane found but this is not the correct axis direction
757 }
758 let mut found_axis_index: Option<usize> = None;
759 if !found_pane {
760 for (i, pa) in self.members.iter_mut().enumerate() {
761 if let Member::Axis(pa) = pa
762 && let Some(done) = pa.resize(pane, axis, amount, bounds)
763 {
764 if done {
765 return Some(true); // pane found and operations already done
766 } else if self.axis != axis {
767 return Some(false); // pane found but this is not the correct axis direction
768 } else {
769 found_axis_index = Some(i); // pane found and this is correct direction
770 }
771 }
772 }
773 found_axis_index?; // no pane found
774 }
775
776 let min_size = match axis {
777 Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
778 Axis::Vertical => px(VERTICAL_MIN_SIZE),
779 };
780 let mut flexes = self.flexes.lock();
781
782 let ix = if found_pane {
783 self.members.iter().position(|m| {
784 if let Member::Pane(p) = m {
785 p == pane
786 } else {
787 false
788 }
789 })
790 } else {
791 found_axis_index
792 };
793
794 if ix.is_none() {
795 return Some(true);
796 }
797
798 let ix = ix.unwrap_or(0);
799
800 let size = move |ix, flexes: &[f32]| {
801 container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
802 };
803
804 // Don't allow resizing to less than the minimum size, if elements are already too small
805 if min_size - px(1.) > size(ix, flexes.as_slice()) {
806 return Some(true);
807 }
808
809 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
810 let flex_change = flexes.len() as f32 * pixel_dx / container_size.along(axis);
811 let current_target_flex = flexes[target_ix] + flex_change;
812 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
813 (current_target_flex, next_target_flex)
814 };
815
816 let apply_changes =
817 |current_ix: usize, proposed_current_pixel_change: Pixels, flexes: &mut [f32]| {
818 let next_target_size = Pixels::max(
819 size(current_ix + 1, flexes) - proposed_current_pixel_change,
820 min_size,
821 );
822 let current_target_size = Pixels::max(
823 size(current_ix, flexes) + size(current_ix + 1, flexes) - next_target_size,
824 min_size,
825 );
826
827 let current_pixel_change = current_target_size - size(current_ix, flexes);
828
829 let (current_target_flex, next_target_flex) =
830 flex_changes(current_pixel_change, current_ix, 1, flexes);
831
832 flexes[current_ix] = current_target_flex;
833 flexes[current_ix + 1] = next_target_flex;
834 };
835
836 if ix + 1 == flexes.len() {
837 apply_changes(ix - 1, -1.0 * amount, flexes.as_mut_slice());
838 } else {
839 apply_changes(ix, amount, flexes.as_mut_slice());
840 }
841 Some(true)
842 }
843
844 fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>) {
845 for member in self.members.iter_mut() {
846 match member {
847 Member::Axis(axis) => axis.swap(from, to),
848 Member::Pane(pane) => {
849 if pane == from {
850 *member = Member::Pane(to.clone());
851 } else if pane == to {
852 *member = Member::Pane(from.clone())
853 }
854 }
855 }
856 }
857 }
858
859 fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
860 debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
861
862 for (idx, member) in self.members.iter().enumerate() {
863 match member {
864 Member::Pane(found) => {
865 if pane == found {
866 return self.bounding_boxes.lock()[idx];
867 }
868 }
869 Member::Axis(axis) => {
870 if let Some(rect) = axis.bounding_box_for_pane(pane) {
871 return Some(rect);
872 }
873 }
874 }
875 }
876 None
877 }
878
879 fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&Entity<Pane>> {
880 debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
881
882 let bounding_boxes = self.bounding_boxes.lock();
883
884 for (idx, member) in self.members.iter().enumerate() {
885 if let Some(coordinates) = bounding_boxes[idx]
886 && coordinates.contains(&coordinate)
887 {
888 return match member {
889 Member::Pane(found) => Some(found),
890 Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
891 };
892 }
893 }
894 None
895 }
896
897 fn render(
898 &self,
899 basis: usize,
900 zoomed: Option<&AnyWeakView>,
901 render_cx: &dyn PaneLeaderDecorator,
902 window: &mut Window,
903 cx: &mut App,
904 ) -> PaneRenderResult {
905 debug_assert!(self.members.len() == self.flexes.lock().len());
906 let mut active_pane_ix = None;
907 let mut contains_active_pane = false;
908 let mut is_leaf_pane = vec![false; self.members.len()];
909
910 let rendered_children = self
911 .members
912 .iter()
913 .enumerate()
914 .map(|(ix, member)| {
915 match member {
916 Member::Pane(pane) => {
917 is_leaf_pane[ix] = true;
918 if pane == render_cx.active_pane() {
919 active_pane_ix = Some(ix);
920 contains_active_pane = true;
921 }
922 }
923 Member::Axis(_) => {
924 is_leaf_pane[ix] = false;
925 }
926 }
927
928 let result = member.render((basis + ix) * 10, zoomed, render_cx, window, cx);
929 if result.contains_active_pane {
930 contains_active_pane = true;
931 }
932 result.element.into_any_element()
933 })
934 .collect::<Vec<_>>();
935
936 let element = pane_axis(
937 self.axis,
938 basis,
939 self.flexes.clone(),
940 self.bounding_boxes.clone(),
941 render_cx.workspace().clone(),
942 )
943 .with_is_leaf_pane_mask(is_leaf_pane)
944 .children(rendered_children)
945 .with_active_pane(active_pane_ix)
946 .into_any_element();
947
948 PaneRenderResult {
949 element,
950 contains_active_pane,
951 }
952 }
953}
954
955#[derive(Clone, Copy, Debug, Deserialize, PartialEq, JsonSchema)]
956#[serde(rename_all = "snake_case")]
957pub enum SplitDirection {
958 Up,
959 Down,
960 Left,
961 Right,
962}
963
964impl std::fmt::Display for SplitDirection {
965 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
966 match self {
967 SplitDirection::Up => write!(f, "up"),
968 SplitDirection::Down => write!(f, "down"),
969 SplitDirection::Left => write!(f, "left"),
970 SplitDirection::Right => write!(f, "right"),
971 }
972 }
973}
974
975impl SplitDirection {
976 pub fn all() -> [Self; 4] {
977 [Self::Up, Self::Down, Self::Left, Self::Right]
978 }
979
980 pub fn vertical(cx: &mut App) -> Self {
981 match WorkspaceSettings::get_global(cx).pane_split_direction_vertical {
982 PaneSplitDirectionVertical::Left => SplitDirection::Left,
983 PaneSplitDirectionVertical::Right => SplitDirection::Right,
984 }
985 }
986
987 pub fn horizontal(cx: &mut App) -> Self {
988 match WorkspaceSettings::get_global(cx).pane_split_direction_horizontal {
989 PaneSplitDirectionHorizontal::Down => SplitDirection::Down,
990 PaneSplitDirectionHorizontal::Up => SplitDirection::Up,
991 }
992 }
993
994 pub fn edge(&self, rect: Bounds<Pixels>) -> Pixels {
995 match self {
996 Self::Up => rect.origin.y,
997 Self::Down => rect.bottom_left().y,
998 Self::Left => rect.bottom_left().x,
999 Self::Right => rect.bottom_right().x,
1000 }
1001 }
1002
1003 pub fn along_edge(&self, bounds: Bounds<Pixels>, length: Pixels) -> Bounds<Pixels> {
1004 match self {
1005 Self::Up => Bounds {
1006 origin: bounds.origin,
1007 size: size(bounds.size.width, length),
1008 },
1009 Self::Down => Bounds {
1010 origin: point(bounds.bottom_left().x, bounds.bottom_left().y - length),
1011 size: size(bounds.size.width, length),
1012 },
1013 Self::Left => Bounds {
1014 origin: bounds.origin,
1015 size: size(length, bounds.size.height),
1016 },
1017 Self::Right => Bounds {
1018 origin: point(bounds.bottom_right().x - length, bounds.bottom_left().y),
1019 size: size(length, bounds.size.height),
1020 },
1021 }
1022 }
1023
1024 pub fn axis(&self) -> Axis {
1025 match self {
1026 Self::Up | Self::Down => Axis::Vertical,
1027 Self::Left | Self::Right => Axis::Horizontal,
1028 }
1029 }
1030
1031 pub fn increasing(&self) -> bool {
1032 match self {
1033 Self::Left | Self::Up => false,
1034 Self::Down | Self::Right => true,
1035 }
1036 }
1037
1038 pub fn opposite(&self) -> SplitDirection {
1039 match self {
1040 Self::Down => Self::Up,
1041 Self::Up => Self::Down,
1042 Self::Left => Self::Right,
1043 Self::Right => Self::Left,
1044 }
1045 }
1046}
1047
1048mod element {
1049 use std::mem;
1050 use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
1051
1052 use gpui::{
1053 Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId,
1054 HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
1055 Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size,
1056 };
1057 use gpui::{CursorStyle, Hitbox};
1058 use parking_lot::Mutex;
1059 use settings::Settings;
1060 use smallvec::SmallVec;
1061 use ui::prelude::*;
1062 use util::ResultExt;
1063
1064 use crate::Workspace;
1065
1066 use crate::WorkspaceSettings;
1067
1068 use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
1069
1070 const DIVIDER_SIZE: f32 = 1.0;
1071
1072 pub(super) fn pane_axis(
1073 axis: Axis,
1074 basis: usize,
1075 flexes: Arc<Mutex<Vec<f32>>>,
1076 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
1077 workspace: WeakEntity<Workspace>,
1078 ) -> PaneAxisElement {
1079 PaneAxisElement {
1080 axis,
1081 basis,
1082 flexes,
1083 bounding_boxes,
1084 children: SmallVec::new(),
1085 active_pane_ix: None,
1086 workspace,
1087 is_leaf_pane_mask: Vec::new(),
1088 }
1089 }
1090
1091 pub struct PaneAxisElement {
1092 axis: Axis,
1093 basis: usize,
1094 /// Equivalent to ColumnWidths (but in terms of flexes instead of percentages)
1095 /// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%"
1096 flexes: Arc<Mutex<Vec<f32>>>,
1097 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
1098 children: SmallVec<[AnyElement; 2]>,
1099 active_pane_ix: Option<usize>,
1100 workspace: WeakEntity<Workspace>,
1101 // Track which children are leaf panes (Member::Pane) vs axes (Member::Axis)
1102 is_leaf_pane_mask: Vec<bool>,
1103 }
1104
1105 pub struct PaneAxisLayout {
1106 dragged_handle: Rc<RefCell<Option<usize>>>,
1107 children: Vec<PaneAxisChildLayout>,
1108 }
1109
1110 struct PaneAxisChildLayout {
1111 bounds: Bounds<Pixels>,
1112 element: AnyElement,
1113 handle: Option<PaneAxisHandleLayout>,
1114 is_leaf_pane: bool,
1115 }
1116
1117 struct PaneAxisHandleLayout {
1118 hitbox: Hitbox,
1119 divider_bounds: Bounds<Pixels>,
1120 }
1121
1122 impl PaneAxisElement {
1123 pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
1124 self.active_pane_ix = active_pane_ix;
1125 self
1126 }
1127
1128 pub fn with_is_leaf_pane_mask(mut self, mask: Vec<bool>) -> Self {
1129 self.is_leaf_pane_mask = mask;
1130 self
1131 }
1132
1133 fn compute_resize(
1134 flexes: &Arc<Mutex<Vec<f32>>>,
1135 e: &MouseMoveEvent,
1136 ix: usize,
1137 axis: Axis,
1138 child_start: Point<Pixels>,
1139 container_size: Size<Pixels>,
1140 workspace: WeakEntity<Workspace>,
1141 window: &mut Window,
1142 cx: &mut App,
1143 ) {
1144 let min_size = match axis {
1145 Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
1146 Axis::Vertical => px(VERTICAL_MIN_SIZE),
1147 };
1148 let mut flexes = flexes.lock();
1149 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
1150
1151 // Math to convert a flex value to a pixel value
1152 let size = move |ix, flexes: &[f32]| {
1153 container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
1154 };
1155
1156 // Don't allow resizing to less than the minimum size, if elements are already too small
1157 if min_size - px(1.) > size(ix, flexes.as_slice()) {
1158 return;
1159 }
1160
1161 // This is basically a "bucket" of pixel changes that need to be applied in response to this
1162 // mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels
1163 let mut proposed_current_pixel_change =
1164 (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
1165
1166 // This takes a pixel change, and computes the flex changes that correspond to this pixel change
1167 // as well as the next one, for some reason
1168 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
1169 let flex_change = pixel_dx / container_size.along(axis);
1170 let current_target_flex = flexes[target_ix] + flex_change;
1171 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
1172 (current_target_flex, next_target_flex)
1173 };
1174
1175 // Generate the list of flex successors, from the current index.
1176 // If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6]
1177 // If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0]
1178 let mut successors = iter::from_fn({
1179 let forward = proposed_current_pixel_change > px(0.);
1180 let mut ix_offset = 0;
1181 let len = flexes.len();
1182 move || {
1183 let result = if forward {
1184 (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
1185 } else {
1186 (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
1187 };
1188
1189 ix_offset += 1;
1190
1191 result
1192 }
1193 });
1194
1195 // Now actually loop over these, and empty our bucket of pixel changes
1196 while proposed_current_pixel_change.abs() > px(0.) {
1197 let Some(current_ix) = successors.next() else {
1198 break;
1199 };
1200
1201 let next_target_size = Pixels::max(
1202 size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
1203 min_size,
1204 );
1205
1206 let current_target_size = Pixels::max(
1207 size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
1208 - next_target_size,
1209 min_size,
1210 );
1211
1212 let current_pixel_change =
1213 current_target_size - size(current_ix, flexes.as_slice());
1214
1215 let (current_target_flex, next_target_flex) =
1216 flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
1217
1218 flexes[current_ix] = current_target_flex;
1219 flexes[current_ix + 1] = next_target_flex;
1220
1221 proposed_current_pixel_change -= current_pixel_change;
1222 }
1223
1224 workspace
1225 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1226 .log_err();
1227 cx.stop_propagation();
1228 window.refresh();
1229 }
1230
1231 fn layout_handle(
1232 axis: Axis,
1233 pane_bounds: Bounds<Pixels>,
1234 window: &mut Window,
1235 _cx: &mut App,
1236 ) -> PaneAxisHandleLayout {
1237 let handle_bounds = Bounds {
1238 origin: pane_bounds.origin.apply_along(axis, |origin| {
1239 origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.)
1240 }),
1241 size: pane_bounds
1242 .size
1243 .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)),
1244 };
1245 let divider_bounds = Bounds {
1246 origin: pane_bounds
1247 .origin
1248 .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)),
1249 size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)),
1250 };
1251
1252 PaneAxisHandleLayout {
1253 hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::BlockMouse),
1254 divider_bounds,
1255 }
1256 }
1257 }
1258
1259 impl IntoElement for PaneAxisElement {
1260 type Element = Self;
1261
1262 fn into_element(self) -> Self::Element {
1263 self
1264 }
1265 }
1266
1267 impl Element for PaneAxisElement {
1268 type RequestLayoutState = ();
1269 type PrepaintState = PaneAxisLayout;
1270
1271 fn id(&self) -> Option<ElementId> {
1272 Some(self.basis.into())
1273 }
1274
1275 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1276 None
1277 }
1278
1279 fn request_layout(
1280 &mut self,
1281 _global_id: Option<&GlobalElementId>,
1282 _inspector_id: Option<&gpui::InspectorElementId>,
1283 window: &mut Window,
1284 cx: &mut App,
1285 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
1286 let style = Style {
1287 flex_grow: 1.,
1288 flex_shrink: 1.,
1289 flex_basis: relative(0.).into(),
1290 size: size(relative(1.).into(), relative(1.).into()),
1291 ..Style::default()
1292 };
1293 (window.request_layout(style, None, cx), ())
1294 }
1295
1296 fn prepaint(
1297 &mut self,
1298 global_id: Option<&GlobalElementId>,
1299 _inspector_id: Option<&gpui::InspectorElementId>,
1300 bounds: Bounds<Pixels>,
1301 _state: &mut Self::RequestLayoutState,
1302 window: &mut Window,
1303 cx: &mut App,
1304 ) -> PaneAxisLayout {
1305 let dragged_handle = window.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
1306 global_id.unwrap(),
1307 |state, _cx| {
1308 let state = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
1309 (state.clone(), state)
1310 },
1311 );
1312 let flexes = self.flexes.lock().clone();
1313 let len = self.children.len();
1314 debug_assert!(flexes.len() == len);
1315 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
1316
1317 let total_flex = len as f32;
1318
1319 let mut origin = bounds.origin;
1320 let space_per_flex = bounds.size.along(self.axis) / total_flex;
1321
1322 let mut bounding_boxes = self.bounding_boxes.lock();
1323 bounding_boxes.clear();
1324
1325 let mut layout = PaneAxisLayout {
1326 dragged_handle,
1327 children: Vec::new(),
1328 };
1329 for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() {
1330 let child_flex = flexes[ix];
1331
1332 let child_size = bounds
1333 .size
1334 .apply_along(self.axis, |_| space_per_flex * child_flex)
1335 .map(|d| d.round());
1336
1337 let child_bounds = Bounds {
1338 origin,
1339 size: child_size,
1340 };
1341
1342 bounding_boxes.push(Some(child_bounds));
1343 child.layout_as_root(child_size.into(), window, cx);
1344 child.prepaint_at(origin, window, cx);
1345
1346 origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
1347
1348 let is_leaf_pane = self.is_leaf_pane_mask.get(ix).copied().unwrap_or(true);
1349
1350 layout.children.push(PaneAxisChildLayout {
1351 bounds: child_bounds,
1352 element: child,
1353 handle: None,
1354 is_leaf_pane,
1355 })
1356 }
1357
1358 for (ix, child_layout) in layout.children.iter_mut().enumerate() {
1359 if ix < len - 1 {
1360 child_layout.handle = Some(Self::layout_handle(
1361 self.axis,
1362 child_layout.bounds,
1363 window,
1364 cx,
1365 ));
1366 }
1367 }
1368
1369 layout
1370 }
1371
1372 fn paint(
1373 &mut self,
1374 _id: Option<&GlobalElementId>,
1375 _inspector_id: Option<&gpui::InspectorElementId>,
1376 bounds: gpui::Bounds<ui::prelude::Pixels>,
1377 _: &mut Self::RequestLayoutState,
1378 layout: &mut Self::PrepaintState,
1379 window: &mut Window,
1380 cx: &mut App,
1381 ) {
1382 for child in &mut layout.children {
1383 child.element.paint(window, cx);
1384 }
1385
1386 let overlay_opacity = WorkspaceSettings::get(None, cx)
1387 .active_pane_modifiers
1388 .inactive_opacity
1389 .map(|val| val.0.clamp(0.0, 1.0))
1390 .and_then(|val| (val <= 1.).then_some(val));
1391
1392 let mut overlay_background = cx.theme().colors().editor_background;
1393 if let Some(opacity) = overlay_opacity {
1394 overlay_background.fade_out(opacity);
1395 }
1396
1397 let overlay_border = WorkspaceSettings::get(None, cx)
1398 .active_pane_modifiers
1399 .border_size
1400 .and_then(|val| (val >= 0.).then_some(val));
1401
1402 for (ix, child) in &mut layout.children.iter_mut().enumerate() {
1403 if overlay_opacity.is_some() || overlay_border.is_some() {
1404 // the overlay has to be painted in origin+1px with size width-1px
1405 // in order to accommodate the divider between panels
1406 let overlay_bounds = Bounds {
1407 origin: child
1408 .bounds
1409 .origin
1410 .apply_along(Axis::Horizontal, |val| val + px(1.)),
1411 size: child
1412 .bounds
1413 .size
1414 .apply_along(Axis::Horizontal, |val| val - px(1.)),
1415 };
1416
1417 if overlay_opacity.is_some()
1418 && child.is_leaf_pane
1419 && self.active_pane_ix != Some(ix)
1420 {
1421 window.paint_quad(gpui::fill(overlay_bounds, overlay_background));
1422 }
1423
1424 if let Some(border) = overlay_border
1425 && self.active_pane_ix == Some(ix)
1426 && child.is_leaf_pane
1427 {
1428 window.paint_quad(gpui::quad(
1429 overlay_bounds,
1430 0.,
1431 gpui::transparent_black(),
1432 border,
1433 cx.theme().colors().border_selected,
1434 BorderStyle::Solid,
1435 ));
1436 }
1437 }
1438
1439 if let Some(handle) = child.handle.as_mut() {
1440 let cursor_style = match self.axis {
1441 Axis::Vertical => CursorStyle::ResizeRow,
1442 Axis::Horizontal => CursorStyle::ResizeColumn,
1443 };
1444
1445 if layout
1446 .dragged_handle
1447 .borrow()
1448 .is_some_and(|dragged_ix| dragged_ix == ix)
1449 {
1450 window.set_window_cursor_style(cursor_style);
1451 } else {
1452 window.set_cursor_style(cursor_style, &handle.hitbox);
1453 }
1454
1455 window.paint_quad(gpui::fill(
1456 handle.divider_bounds,
1457 cx.theme().colors().pane_group_border,
1458 ));
1459
1460 window.on_mouse_event({
1461 let dragged_handle = layout.dragged_handle.clone();
1462 let flexes = self.flexes.clone();
1463 let workspace = self.workspace.clone();
1464 let handle_hitbox = handle.hitbox.clone();
1465 move |e: &MouseDownEvent, phase, window, cx| {
1466 if phase.bubble() && handle_hitbox.is_hovered(window) {
1467 dragged_handle.replace(Some(ix));
1468 if e.click_count >= 2 {
1469 let mut borrow = flexes.lock();
1470 *borrow = vec![1.; borrow.len()];
1471 workspace
1472 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1473 .log_err();
1474
1475 window.refresh();
1476 }
1477 cx.stop_propagation();
1478 }
1479 }
1480 });
1481 window.on_mouse_event({
1482 let workspace = self.workspace.clone();
1483 let dragged_handle = layout.dragged_handle.clone();
1484 let flexes = self.flexes.clone();
1485 let child_bounds = child.bounds;
1486 let axis = self.axis;
1487 move |e: &MouseMoveEvent, phase, window, cx| {
1488 let dragged_handle = dragged_handle.borrow();
1489 if phase.bubble() && *dragged_handle == Some(ix) {
1490 Self::compute_resize(
1491 &flexes,
1492 e,
1493 ix,
1494 axis,
1495 child_bounds.origin,
1496 bounds.size,
1497 workspace.clone(),
1498 window,
1499 cx,
1500 )
1501 }
1502 }
1503 });
1504 }
1505 }
1506
1507 window.on_mouse_event({
1508 let dragged_handle = layout.dragged_handle.clone();
1509 move |_: &MouseUpEvent, phase, _window, _cx| {
1510 if phase.bubble() {
1511 dragged_handle.replace(None);
1512 }
1513 }
1514 });
1515 }
1516 }
1517
1518 impl ParentElement for PaneAxisElement {
1519 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1520 self.children.extend(elements)
1521 }
1522 }
1523
1524 fn flex_values_in_bounds(flexes: &[f32]) -> bool {
1525 (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
1526 }
1527}