1use crate::{format_err, Error, Result};
5use cassowary::strength::{REQUIRED, STRONG, WEAK};
6use cassowary::WeightedRelation::*;
7use cassowary::{AddConstraintError, Expression, Solver, SuggestValueError, Variable};
8use std::collections::HashMap;
9
10use crate::widgets::{Rect, WidgetId};
11
12fn unwrap_variable_or(var: Option<Variable>, value: f64) -> Expression {
17 var.map(|v| v + 0.0)
19 .unwrap_or_else(|| Expression::from_constant(value))
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum DimensionSpec {
27 Fixed(u16),
29 Percentage(u8),
31}
32
33impl Default for DimensionSpec {
34 fn default() -> Self {
35 DimensionSpec::Percentage(100)
36 }
37}
38
39#[derive(Clone, Default, Copy, Debug, PartialEq, Eq)]
43pub struct Dimension {
44 pub spec: DimensionSpec,
45 pub maximum: Option<u16>,
46 pub minimum: Option<u16>,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum ChildOrientation {
54 Vertical,
55 Horizontal,
56}
57
58impl Default for ChildOrientation {
59 fn default() -> Self {
60 ChildOrientation::Horizontal
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum VerticalAlignment {
69 Top,
70 Middle,
71 Bottom,
72}
73
74impl Default for VerticalAlignment {
75 fn default() -> Self {
76 VerticalAlignment::Top
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum HorizontalAlignment {
85 Left,
86 Center,
87 Right,
88}
89
90impl Default for HorizontalAlignment {
91 fn default() -> Self {
92 HorizontalAlignment::Left
93 }
94}
95
96#[derive(Clone, Default, Copy, Debug, PartialEq, Eq)]
98pub struct Constraints {
99 pub width: Dimension,
100 pub height: Dimension,
101 pub valign: VerticalAlignment,
102 pub halign: HorizontalAlignment,
103 pub child_orientation: ChildOrientation,
104}
105
106impl Constraints {
107 pub fn with_fixed_width_height(width: u16, height: u16) -> Self {
108 *Self::default()
109 .set_fixed_width(width)
110 .set_fixed_height(height)
111 }
112
113 pub fn set_fixed_width(&mut self, width: u16) -> &mut Self {
114 self.width = Dimension {
115 spec: DimensionSpec::Fixed(width),
116 minimum: Some(width),
117 maximum: Some(width),
118 };
119 self
120 }
121
122 pub fn set_pct_width(&mut self, width: u8) -> &mut Self {
123 self.width = Dimension {
124 spec: DimensionSpec::Percentage(width),
125 ..Default::default()
126 };
127 self
128 }
129
130 pub fn set_fixed_height(&mut self, height: u16) -> &mut Self {
131 self.height = Dimension {
132 spec: DimensionSpec::Fixed(height),
133 minimum: Some(height),
134 maximum: Some(height),
135 };
136 self
137 }
138
139 pub fn set_pct_height(&mut self, height: u8) -> &mut Self {
140 self.height = Dimension {
141 spec: DimensionSpec::Percentage(height),
142 ..Default::default()
143 };
144 self
145 }
146
147 pub fn set_valign(&mut self, valign: VerticalAlignment) -> &mut Self {
148 self.valign = valign;
149 self
150 }
151
152 pub fn set_halign(&mut self, halign: HorizontalAlignment) -> &mut Self {
153 self.halign = halign;
154 self
155 }
156}
157
158pub struct LayoutState {
160 solver: Solver,
161 screen_width: Variable,
162 screen_height: Variable,
163 widget_states: HashMap<WidgetId, WidgetState>,
164}
165
166#[derive(Clone)]
171struct WidgetState {
172 left: Variable,
173 top: Variable,
174 width: Variable,
175 height: Variable,
176 constraints: Constraints,
177 children: Vec<WidgetId>,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct LaidOutWidget {
182 pub widget: WidgetId,
183 pub rect: Rect,
184}
185
186fn suggesterr(e: SuggestValueError) -> Error {
187 match e {
188 SuggestValueError::UnknownEditVariable => format_err!("Unknown edit variable"),
189 SuggestValueError::InternalSolverError(e) => format_err!("Internal solver error: {}", e),
190 }
191}
192
193fn adderr(e: AddConstraintError) -> Error {
194 format_err!("{:?}", e)
195}
196
197impl Default for LayoutState {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203impl LayoutState {
204 pub fn new() -> Self {
206 let mut solver = Solver::new();
207 let screen_width = Variable::new();
208 let screen_height = Variable::new();
209 solver
210 .add_edit_variable(screen_width, STRONG)
211 .expect("failed to add screen_width to solver");
212 solver
213 .add_edit_variable(screen_height, STRONG)
214 .expect("failed to add screen_height to solver");
215 Self {
216 solver,
217 screen_width,
218 screen_height,
219 widget_states: HashMap::new(),
220 }
221 }
222
223 pub fn add_widget(
225 &mut self,
226 widget: WidgetId,
227 constraints: &Constraints,
228 children: &[WidgetId],
229 ) {
230 let state = WidgetState {
231 left: Variable::new(),
232 top: Variable::new(),
233 width: Variable::new(),
234 height: Variable::new(),
235 constraints: *constraints,
236 children: children.to_vec(),
237 };
238 self.widget_states.insert(widget, state);
239 }
240
241 pub fn compute_constraints(
245 &mut self,
246 screen_width: usize,
247 screen_height: usize,
248 root_widget: WidgetId,
249 ) -> Result<Vec<LaidOutWidget>> {
250 self.solver
251 .suggest_value(self.screen_width, screen_width as f64)
252 .map_err(suggesterr)?;
253 self.solver
254 .suggest_value(self.screen_height, screen_height as f64)
255 .map_err(suggesterr)?;
256
257 let width = self.screen_width;
258 let height = self.screen_height;
259 self.update_widget_constraint(root_widget, width, height, None, None)?;
260
261 self.solver.fetch_changes();
269
270 let mut results = Vec::new();
271 self.compute_widget_state(root_widget, 0, 0, &mut results)?;
272
273 Ok(results)
274 }
275
276 fn compute_widget_state(
277 &self,
278 widget: WidgetId,
279 parent_left: usize,
280 parent_top: usize,
281 results: &mut Vec<LaidOutWidget>,
282 ) -> Result<()> {
283 let state = self
284 .widget_states
285 .get(&widget)
286 .ok_or_else(|| format_err!("widget has no solver state"))?;
287 let width = self.solver.get_value(state.width) as usize;
288 let height = self.solver.get_value(state.height) as usize;
289 let left = self.solver.get_value(state.left) as usize;
290 let top = self.solver.get_value(state.top) as usize;
291
292 results.push(LaidOutWidget {
293 widget,
294 rect: Rect {
295 x: left - parent_left,
296 y: top - parent_top,
297 width,
298 height,
299 },
300 });
301
302 for child in &state.children {
303 self.compute_widget_state(*child, left, top, results)?;
304 }
305
306 Ok(())
307 }
308
309 fn update_widget_constraint(
310 &mut self,
311 widget: WidgetId,
312 parent_width: Variable,
313 parent_height: Variable,
314 parent_left: Option<Variable>,
315 parent_top: Option<Variable>,
316 ) -> Result<WidgetState> {
317 let state = self
318 .widget_states
319 .get(&widget)
320 .ok_or_else(|| format_err!("widget has no solver state"))?
321 .clone();
322
323 let is_root_widget = parent_left.is_none();
324
325 let parent_left = unwrap_variable_or(parent_left, 0.0);
326 let parent_top = unwrap_variable_or(parent_top, 0.0);
327
328 self.solver
330 .add_constraint(
331 (state.left + state.width) | LE(REQUIRED) | (parent_left.clone() + parent_width),
332 )
333 .map_err(adderr)?;
334 self.solver
335 .add_constraint(state.left | GE(REQUIRED) | parent_left)
336 .map_err(adderr)?;
337
338 self.solver
339 .add_constraint(
340 (state.top + state.height) | LE(REQUIRED) | (parent_top.clone() + parent_height),
341 )
342 .map_err(adderr)?;
343 self.solver
344 .add_constraint(state.top | GE(REQUIRED) | parent_top)
345 .map_err(adderr)?;
346
347 if is_root_widget {
348 match state.constraints.halign {
351 HorizontalAlignment::Left => self
352 .solver
353 .add_constraint(state.left | EQ(STRONG) | 0.0)
354 .map_err(adderr)?,
355 HorizontalAlignment::Right => self
356 .solver
357 .add_constraint(state.left | EQ(STRONG) | (parent_width - state.width))
358 .map_err(adderr)?,
359 HorizontalAlignment::Center => self
360 .solver
361 .add_constraint(state.left | EQ(STRONG) | ((parent_width - state.width) / 2.0))
362 .map_err(adderr)?,
363 }
364
365 match state.constraints.valign {
366 VerticalAlignment::Top => self
367 .solver
368 .add_constraint(state.top | EQ(STRONG) | 0.0)
369 .map_err(adderr)?,
370 VerticalAlignment::Bottom => self
371 .solver
372 .add_constraint(state.top | EQ(STRONG) | (parent_height - state.height))
373 .map_err(adderr)?,
374 VerticalAlignment::Middle => self
375 .solver
376 .add_constraint(state.top | EQ(STRONG) | ((parent_height - state.height) / 2.0))
377 .map_err(adderr)?,
378 }
379 }
380
381 match state.constraints.width.spec {
382 DimensionSpec::Fixed(width) => {
383 self.solver
384 .add_constraint(state.width | EQ(STRONG) | f64::from(width))
385 .map_err(adderr)?;
386 }
387 DimensionSpec::Percentage(pct) => {
388 self.solver
389 .add_constraint(
390 state.width | EQ(STRONG) | (f64::from(pct) * parent_width / 100.0),
391 )
392 .map_err(adderr)?;
393 }
394 }
395 self.solver
396 .add_constraint(
397 state.width
398 | GE(STRONG)
399 | f64::from(state.constraints.width.minimum.unwrap_or(1).max(1)),
400 )
401 .map_err(adderr)?;
402 if let Some(max_width) = state.constraints.width.maximum {
403 self.solver
404 .add_constraint(state.width | LE(STRONG) | f64::from(max_width))
405 .map_err(adderr)?;
406 }
407
408 match state.constraints.height.spec {
409 DimensionSpec::Fixed(height) => {
410 self.solver
411 .add_constraint(state.height | EQ(STRONG) | f64::from(height))
412 .map_err(adderr)?;
413 }
414 DimensionSpec::Percentage(pct) => {
415 self.solver
416 .add_constraint(
417 state.height | EQ(STRONG) | (f64::from(pct) * parent_height / 100.0),
418 )
419 .map_err(adderr)?;
420 }
421 }
422 self.solver
423 .add_constraint(
424 state.height
425 | GE(STRONG)
426 | f64::from(state.constraints.height.minimum.unwrap_or(1).max(1)),
427 )
428 .map_err(adderr)?;
429 if let Some(max_height) = state.constraints.height.maximum {
430 self.solver
431 .add_constraint(state.height | LE(STRONG) | f64::from(max_height))
432 .map_err(adderr)?;
433 }
434
435 let has_children = !state.children.is_empty();
436 if has_children {
437 let mut left_edge: Expression = state.left + 0.0;
438 let mut top_edge: Expression = state.top + 0.0;
439 let mut width_constraint = Expression::from_constant(0.0);
440 let mut height_constraint = Expression::from_constant(0.0);
441
442 for child in &state.children {
443 let child_state = self.update_widget_constraint(
444 *child,
445 state.width,
446 state.height,
447 Some(state.left),
448 Some(state.top),
449 )?;
450
451 match child_state.constraints.halign {
452 HorizontalAlignment::Left => self
453 .solver
454 .add_constraint(child_state.left | EQ(STRONG) | left_edge.clone())
455 .map_err(adderr)?,
456 HorizontalAlignment::Right => self
457 .solver
458 .add_constraint(
459 (child_state.left + child_state.width)
460 | EQ(STRONG)
461 | (state.left + state.width),
462 )
463 .map_err(adderr)?,
464 HorizontalAlignment::Center => self
465 .solver
466 .add_constraint(
467 child_state.left
468 | EQ(STRONG)
469 | (state.left + (state.width - child_state.width) / 2.0),
470 )
471 .map_err(adderr)?,
472 }
473
474 match child_state.constraints.valign {
475 VerticalAlignment::Top => self
476 .solver
477 .add_constraint(child_state.top | EQ(STRONG) | top_edge.clone())
478 .map_err(adderr)?,
479 VerticalAlignment::Bottom => self
480 .solver
481 .add_constraint(
482 (child_state.top + child_state.height)
483 | EQ(STRONG)
484 | (state.top + state.height),
485 )
486 .map_err(adderr)?,
487 VerticalAlignment::Middle => self
488 .solver
489 .add_constraint(
490 child_state.top
491 | EQ(STRONG)
492 | (state.top + (state.height - child_state.height) / 2.0),
493 )
494 .map_err(adderr)?,
495 }
496
497 match state.constraints.child_orientation {
498 ChildOrientation::Horizontal => {
499 left_edge = child_state.left + child_state.width;
500 width_constraint = width_constraint + child_state.width;
501 }
502 ChildOrientation::Vertical => {
503 top_edge = child_state.top + child_state.height;
504 height_constraint = height_constraint + child_state.height;
505 }
506 }
507 }
508
509 self.solver
512 .add_constraint(left_edge | EQ(STRONG) | (state.left + state.width))
513 .map_err(adderr)?;
514
515 self.solver
516 .add_constraint(state.width | GE(WEAK) | width_constraint)
517 .map_err(adderr)?;
518
519 self.solver
522 .add_constraint(top_edge | EQ(STRONG) | (state.top + state.height))
523 .map_err(adderr)?;
524
525 self.solver
526 .add_constraint(state.height | GE(WEAK) | height_constraint)
527 .map_err(adderr)?;
528 }
529
530 Ok(state)
531 }
532}
533
534#[cfg(test)]
535mod test {
536 use super::*;
537
538 #[test]
539 fn single_widget_unspec() {
540 let mut layout = LayoutState::new();
541 let main_id = WidgetId::new();
542 layout.add_widget(main_id, &Constraints::default(), &[]);
543 let results = layout.compute_constraints(40, 12, main_id).unwrap();
544
545 assert_eq!(
546 results,
547 vec![LaidOutWidget {
548 widget: main_id,
549 rect: Rect {
550 x: 0,
551 y: 0,
552 width: 40,
553 height: 12,
554 },
555 }]
556 );
557 }
558
559 #[test]
560 fn two_children_pct() {
561 let root = WidgetId::new();
562 let a = WidgetId::new();
563 let b = WidgetId::new();
564
565 let mut layout = LayoutState::new();
566 layout.add_widget(root, &Constraints::default(), &[a, b]);
567 layout.add_widget(a, Constraints::default().set_pct_width(50), &[]);
568 layout.add_widget(b, Constraints::default().set_pct_width(50), &[]);
569
570 let results = layout.compute_constraints(100, 100, root).unwrap();
571
572 assert_eq!(
573 results,
574 vec![
575 LaidOutWidget {
576 widget: root,
577 rect: Rect {
578 x: 0,
579 y: 0,
580 width: 100,
581 height: 100,
582 },
583 },
584 LaidOutWidget {
585 widget: a,
586 rect: Rect {
587 x: 0,
588 y: 0,
589 width: 50,
590 height: 100,
591 },
592 },
593 LaidOutWidget {
594 widget: b,
595 rect: Rect {
596 x: 50,
597 y: 0,
598 width: 50,
599 height: 100,
600 },
601 },
602 ]
603 );
604 }
605
606 #[test]
607 fn three_children_pct() {
608 let root = WidgetId::new();
609 let a = WidgetId::new();
610 let b = WidgetId::new();
611 let c = WidgetId::new();
612
613 let mut layout = LayoutState::new();
614 layout.add_widget(root, &Constraints::default(), &[a, b, c]);
615 layout.add_widget(a, Constraints::default().set_pct_width(20), &[]);
616 layout.add_widget(b, Constraints::default().set_pct_width(20), &[]);
617 layout.add_widget(c, Constraints::default().set_pct_width(20), &[]);
618
619 let results = layout.compute_constraints(100, 100, root).unwrap();
620
621 assert_eq!(
622 results,
623 vec![
624 LaidOutWidget {
625 widget: root,
626 rect: Rect {
627 x: 0,
628 y: 0,
629 width: 100,
630 height: 100,
631 },
632 },
633 LaidOutWidget {
634 widget: a,
635 rect: Rect {
636 x: 0,
637 y: 0,
638 width: 20,
639 height: 100,
640 },
641 },
642 LaidOutWidget {
643 widget: b,
644 rect: Rect {
645 x: 20,
646 y: 0,
647 width: 20,
648 height: 100,
649 },
650 },
651 LaidOutWidget {
652 widget: c,
653 rect: Rect {
654 x: 40,
655 y: 0,
656 width: 20,
657 height: 100,
658 },
659 },
660 ]
661 );
662 }
663
664 #[test]
665 fn two_children_a_b() {
666 let root = WidgetId::new();
667 let a = WidgetId::new();
668 let b = WidgetId::new();
669
670 let mut layout = LayoutState::new();
671 layout.add_widget(root, &Constraints::default(), &[a, b]);
672 layout.add_widget(a, &Constraints::with_fixed_width_height(5, 2), &[]);
673 layout.add_widget(b, &Constraints::with_fixed_width_height(3, 2), &[]);
674
675 let results = layout.compute_constraints(100, 100, root).unwrap();
676
677 assert_eq!(
678 results,
679 vec![
680 LaidOutWidget {
681 widget: root,
682 rect: Rect {
683 x: 0,
684 y: 0,
685 width: 100,
686 height: 100,
687 },
688 },
689 LaidOutWidget {
690 widget: a,
691 rect: Rect {
692 x: 0,
693 y: 0,
694 width: 5,
695 height: 2,
696 },
697 },
698 LaidOutWidget {
699 widget: b,
700 rect: Rect {
701 x: 5,
702 y: 0,
703 width: 3,
704 height: 2,
705 },
706 },
707 ]
708 );
709 }
710
711 #[test]
712 fn two_children_b_a() {
713 let root = WidgetId::new();
714 let a = WidgetId::new();
715 let b = WidgetId::new();
716
717 let mut layout = LayoutState::new();
718 layout.add_widget(root, &Constraints::default(), &[b, a]);
719 layout.add_widget(a, &Constraints::with_fixed_width_height(5, 2), &[]);
720 layout.add_widget(b, &Constraints::with_fixed_width_height(3, 2), &[]);
721
722 let results = layout.compute_constraints(100, 100, root).unwrap();
723
724 assert_eq!(
725 results,
726 vec![
727 LaidOutWidget {
728 widget: root,
729 rect: Rect {
730 x: 0,
731 y: 0,
732 width: 100,
733 height: 100,
734 },
735 },
736 LaidOutWidget {
737 widget: b,
738 rect: Rect {
739 x: 0,
740 y: 0,
741 width: 3,
742 height: 2,
743 },
744 },
745 LaidOutWidget {
746 widget: a,
747 rect: Rect {
748 x: 3,
749 y: 0,
750 width: 5,
751 height: 2,
752 },
753 },
754 ]
755 );
756 }
757
758 #[test]
759 fn two_children_overflow() {
760 let root = WidgetId::new();
761 let a = WidgetId::new();
762 let b = WidgetId::new();
763
764 let mut layout = LayoutState::new();
765 layout.add_widget(root, &Constraints::default(), &[a, b]);
766 layout.add_widget(a, &Constraints::with_fixed_width_height(5, 2), &[]);
767 layout.add_widget(b, &Constraints::with_fixed_width_height(3, 2), &[]);
768
769 let results = layout.compute_constraints(6, 2, root).unwrap();
770
771 assert_eq!(
772 results,
773 vec![
774 LaidOutWidget {
775 widget: root,
776 rect: Rect {
777 x: 0,
778 y: 0,
779 width: 8,
780 height: 2,
781 },
782 },
783 LaidOutWidget {
784 widget: a,
785 rect: Rect {
786 x: 0,
787 y: 0,
788 width: 5,
789 height: 2,
790 },
791 },
792 LaidOutWidget {
793 widget: b,
794 rect: Rect {
795 x: 5,
796 y: 0,
797 width: 3,
798 height: 2,
799 },
800 },
801 ]
802 );
803 }
804
805 macro_rules! single_constrain {
806 ($name:ident, $constraint:expr, $width:expr, $height:expr, $x:expr, $y:expr) => {
807 #[test]
808 fn $name() {
809 let root = WidgetId::new();
810 let mut layout = LayoutState::new();
811 layout.add_widget(root, &$constraint, &[]);
812
813 let results = layout.compute_constraints(100, 100, root).unwrap();
814
815 assert_eq!(
816 results,
817 vec![LaidOutWidget {
818 widget: root,
819 rect: Rect {
820 x: $x,
821 y: $y,
822 width: $width,
823 height: $height,
824 },
825 }]
826 );
827 }
828 };
829 }
830
831 single_constrain!(
832 single_constrained_widget_top,
833 Constraints::with_fixed_width_height(10, 2),
834 10,
835 2,
836 0,
837 0
838 );
839
840 single_constrain!(
841 single_constrained_widget_bottom,
842 Constraints::with_fixed_width_height(10, 2)
843 .set_valign(VerticalAlignment::Bottom)
844 .clone(),
845 10,
846 2,
847 0,
848 98
849 );
850
851 single_constrain!(
852 single_constrained_widget_middle,
853 Constraints::with_fixed_width_height(10, 2)
854 .set_valign(VerticalAlignment::Middle)
855 .clone(),
856 10,
857 2,
858 0,
859 49
860 );
861
862 single_constrain!(
863 single_constrained_widget_right,
864 Constraints::with_fixed_width_height(10, 2)
865 .set_halign(HorizontalAlignment::Right)
866 .clone(),
867 10,
868 2,
869 90,
870 0
871 );
872
873 single_constrain!(
874 single_constrained_widget_bottom_center,
875 Constraints::with_fixed_width_height(10, 2)
876 .set_valign(VerticalAlignment::Bottom)
877 .set_halign(HorizontalAlignment::Center)
878 .clone(),
879 10,
880 2,
881 45,
882 98
883 );
884}