Skip to main content

flux_tui/
common.rs

1use std::error::Error;
2use std::fmt::{Debug, Display, Formatter};
3use std::io::{BufWriter, Stdout};
4use std::num::NonZero;
5
6use arrayvec::ArrayString;
7use bitflags::bitflags;
8use crossterm::QueueableCommand;
9use crossterm::style::{self, Attribute};
10use unicode_segmentation::UnicodeSegmentation;
11use unicode_width::UnicodeWidthStr;
12use vector2d::Vector2D;
13
14pub use arrayvec_const as arrayvec;
15pub use as_any;
16pub use crossterm;
17pub use vector2d;
18
19pub use crossterm::event::Event;
20pub use crossterm::style::Color;
21
22/// Terminal size limitation
23pub type TSize = u16;
24/// Point/Coordinate inside a terminal
25pub type TPoint = Vector2D<TSize>;
26
27/// Direction of characters being drawn
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum CharDirection {
30	/// Left to Right
31	LeftRight,
32	/// Right to Left
33	RightLeft,
34}
35
36/// Generic over the concept of vertical/horizontal
37/// Lower/Higher are relative terms in relation to the screen coordinates
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum Alignment {
40	LowerBound,
41	Center,
42	HigherBound,
43}
44
45impl From<Alignment> for HorizontalAlignment {
46	fn from(value: Alignment) -> Self {
47		match value {
48			Alignment::LowerBound => Self::Left,
49			Alignment::Center => Self::Center,
50			Alignment::HigherBound => Self::Right,
51		}
52	}
53}
54
55impl From<Alignment> for VerticalAlignment {
56	fn from(value: Alignment) -> Self {
57		match value {
58			Alignment::LowerBound => Self::Top,
59			Alignment::Center => Self::Center,
60			Alignment::HigherBound => Self::Bottom,
61		}
62	}
63}
64
65/// Alignment on the x-axis
66#[derive(Clone, Copy, Debug, PartialEq, Default)]
67pub enum HorizontalAlignment {
68	Left,
69	#[default]
70	Center,
71	Right,
72}
73
74impl From<HorizontalAlignment> for Alignment {
75	fn from(value: HorizontalAlignment) -> Self {
76		match value {
77			HorizontalAlignment::Left => Self::HigherBound,
78			HorizontalAlignment::Center => Self::Center,
79			HorizontalAlignment::Right => Self::HigherBound,
80		}
81	}
82}
83
84/// Alignment on the y-axis
85#[derive(Clone, Copy, Debug, PartialEq, Default)]
86pub enum VerticalAlignment {
87	Top,
88	#[default]
89	Center,
90	Bottom,
91}
92
93impl From<VerticalAlignment> for Alignment {
94	fn from(value: VerticalAlignment) -> Self {
95		match value {
96			VerticalAlignment::Top => Self::HigherBound,
97			VerticalAlignment::Center => Self::Center,
98			VerticalAlignment::Bottom => Self::HigherBound,
99		}
100	}
101}
102
103/// Orientation of a component (e.g. StackPanel or ScrollViewer)
104#[derive(Debug, Copy, Clone, PartialEq)]
105#[repr(usize)]
106pub enum Orientation {
107	Horizontal = 0,
108	Vertical,
109}
110
111impl Orientation {
112	const INV_MAP: [Orientation; 2] = [Self::Vertical, Self::Horizontal];
113
114	pub fn invert(&self) -> Self {
115		Self::INV_MAP[*self as usize]
116	}
117}
118
119#[derive(Debug, Default, Copy, Clone, PartialEq)]
120pub struct Line2D<T> {
121	pub a: Vector2D<T>,
122	pub b: Vector2D<T>,
123}
124
125impl Line2D<TSize> {
126	pub const fn new(x1: TSize, y1: TSize, x2: TSize, y2: TSize) -> Self {
127		Self {
128			a: Vector2D::new(x1, y1),
129			b: Vector2D::new(x2, y2),
130		}
131	}
132
133	pub const fn from(p1: TPoint, p2: TPoint) -> Self {
134		Self { a: p1, b: p2 }
135	}
136
137	pub const fn is_vertical(&self) -> bool {
138		self.a.x == self.b.x
139	}
140
141	pub const fn is_horizontal(&self) -> bool {
142		self.a.y == self.b.y
143	}
144
145	pub const fn is_ascending(&self) -> bool {
146		self.a.y < self.b.y
147	}
148
149	pub const fn is_descending(&self) -> bool {
150		self.a.y > self.b.y
151	}
152
153	pub const fn is_constant(&self) -> bool {
154		self.a.y == self.b.y
155	}
156
157	/// Interval = 1
158	pub const fn iter_points(&self) -> PointIterator<Line2D<TSize>> {
159		PointIterator::<Line2D<TSize>>::new(*self)
160	}
161}
162
163pub struct PointIterator<T: Sized> {
164	line: T,
165	cursor: TSize,
166}
167
168impl Iterator for PointIterator<Line2D<TSize>> {
169	type Item = TPoint;
170
171	fn next(&mut self) -> Option<Self::Item> {
172		let mut ret = None;
173		if self.line.is_vertical() {
174			if self.cursor < self.line.b.y {
175				ret = Some(Vector2D::new(self.line.a.y, self.cursor));
176				self.cursor += 1;
177			}
178		}
179		else if self.cursor < self.line.b.x {
180			let div = self.line.b.x - self.line.a.x;
181			let mut m = TSize::MIN;
182			if div != TSize::MIN {
183				m = (self.line.b.y - self.line.a.y) / div;
184			}
185			let b = self.line.a.y - self.line.a.x * m;
186			ret = Some(Vector2D::new(self.cursor, m * self.cursor + b));
187
188			self.cursor += 1;
189		}
190		ret
191	}
192}
193
194impl PointIterator<Line2D<TSize>> {
195	const fn new(line: Line2D<TSize>) -> Self {
196		if line.is_vertical() {
197			Self {
198				line,
199				cursor: line.a.y,
200			}
201		}
202		else {
203			Self {
204				line,
205				cursor: line.a.x,
206			}
207		}
208	}
209}
210
211
212/// Wraps a percentage value
213#[repr(transparent)]
214#[derive(Debug, Clone, Copy, PartialEq)]
215pub struct Percent(NonZero<u8>);
216
217impl Default for Percent {
218	fn default() -> Self {
219		Self::DEFAULT
220	}
221}
222
223impl Percent {
224	pub const MIN: Self = Self(NonZero::<u8>::MIN);
225	pub const MAX: Self = Self(NonZero::new(100).unwrap());
226	pub const DEFAULT: Self = Self::MAX;
227
228	/// The value must be between 1 and 100, otherwise it will get coerced into the range
229	pub const fn from_int(u: u8) -> Self {
230		if u == 0 {
231			Self::MIN
232		}
233		else if u >= 100 {
234			Self::MAX
235		}
236		else {
237			Self(NonZero::new(u).unwrap())
238		}
239	}
240
241	/// The value must be between 0.01 and 1., otherwise it will get coerced into the range
242	pub const fn from_float(f: f32) -> Self {
243		if !f.is_normal() {
244			return Self::DEFAULT;
245		}
246		let u = (f * 100.) as u8;
247		if u == 0 {
248			Self::MIN
249		}
250		else if u >= 100 {
251			Self::MAX
252		}
253		else {
254			Self(NonZero::new(u).unwrap())
255		}
256	}
257
258	/// Multiplies the percentage value with an TSize
259	pub const fn multiply(self, u: TSize) -> TSize {
260		(u * self.0.get() as TSize) / 100
261	}
262
263	/// Inner percentage value as u8
264	pub const fn value(self) -> u8 {
265		self.0.get()
266	}
267}
268
269
270/// Defines the width of a single [Grapheme] - see <https://unicode.org/reports/tr11/>
271#[derive(Copy, Clone, Debug, PartialEq)]
272#[repr(usize)]
273pub enum GlyphWidth {
274	Half = 1,
275	Full = 2,
276}
277
278impl TryFrom<usize> for GlyphWidth {
279	type Error = GraphemeError;
280
281	fn try_from(value: usize) -> Result<Self, GraphemeError> {
282		match value {
283			1 => Ok(GlyphWidth::Half),
284			2 => Ok(GlyphWidth::Full),
285			_ => Err(GraphemeError::ConversionError),
286		}
287	}
288}
289
290impl Default for GlyphWidth {
291	fn default() -> Self {
292		Self::Half
293	}
294}
295
296#[derive(Debug, Clone, Copy, PartialEq)]
297/// Grapheme with calculated width and safety checks
298pub struct Grapheme {
299	inner: ArrayString<{ Self::MAX_SIZE }>,
300	width: GlyphWidth,
301}
302
303impl Default for Grapheme {
304	fn default() -> Self {
305		Self::PLACEHOLDER
306	}
307}
308
309impl Grapheme {
310	pub const MAX_SIZE: usize = 16;
311	pub const PLACEHOLDER: Self = Self::new_unchecked(" ", GlyphWidth::Half);
312	pub const REPLACEMENT: Self = Self::new_unchecked("\u{FFFD}", GlyphWidth::Half);
313
314	//TODO: In case arrayvec_const will get merged, switch back to arrayvec
315	/// This will create a grapheme from a static [str] with a predefined width
316	/// Which will only do the most necessary checks on the grapheme
317	pub(crate) const fn new_unchecked(grapheme: &'static str, width: GlyphWidth) -> Self {
318		if grapheme.len() > Self::MAX_SIZE {
319			panic!(stringify!(GraphemeError::GraphemeTooBig));
320		}
321		if grapheme.as_bytes()[0] < 32 {
322			panic!(stringify!(GraphemeError::InvalidGlyphWidth));
323		}
324		//TODO: Result::unwrap is currently not const
325		match ArrayString::from(grapheme) {
326			Ok(v) => Self { width, inner: v },
327			Err(_) => panic!("Error creating ArrayString"),
328		}
329	}
330
331	/// Creates a new [Grapheme] from a [str].
332	/// Checks that `grapheme` fulfills all checks:
333	/// - Length does not exceed [Self::MAX_SIZE]
334	/// - Contains exactly one grapheme
335	/// - Is not a control char
336	/// - Width is not zero
337	pub fn from(grapheme: &str) -> Result<Self, GraphemeError> {
338		if grapheme.len() > Self::MAX_SIZE {
339			return Err(GraphemeError::GraphemeTooBig);
340		}
341
342		let mut graphemes = grapheme.graphemes(true);
343		let _ = graphemes.next();
344		let g2 = graphemes.next();
345
346		if g2.is_some() {
347			return Err(GraphemeError::TooManyGraphemes);
348		}
349
350		let width = grapheme.width();
351		if grapheme.as_bytes()[0] < 32 || width == 0 {
352			return Err(GraphemeError::InvalidGlyphWidth);
353		}
354
355		Ok(Self {
356			inner: ArrayString::from(grapheme).unwrap(),
357			width: GlyphWidth::try_from(width).unwrap(),
358		})
359	}
360
361	#[inline(always)]
362	pub(crate) fn get_string(&self) -> ArrayString<{ Self::MAX_SIZE }> {
363		self.inner
364	}
365
366	#[inline(always)]
367	pub fn as_str(&self) -> &str {
368		&self.inner
369	}
370
371	#[inline(always)]
372	pub fn width(&self) -> GlyphWidth {
373		self.width
374	}
375}
376
377#[derive(Debug, Copy, Clone, PartialEq)]
378pub enum GraphemeError {
379	GraphemeTooBig,
380	TooManyGraphemes,
381	InvalidGlyphWidth,
382	ConversionError,
383}
384
385impl Display for GraphemeError {
386	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387		f.write_fmt(format_args!("{self:?}"))
388	}
389}
390
391impl Error for GraphemeError {}
392
393bitflags! {
394	/// Styles which just contain the best supported attributed by most terminals
395	/// Used as bitflags seperately
396	#[derive(Debug, Copy, Clone, PartialEq, Default, PartialOrd, Hash)]
397	pub struct Style: u8{
398
399	/// No styles at all, the default
400	const None = 0b0000_0000;
401	/// Resets all styles before drawing the current glyph
402	const ResetBefore = 0b0000_0001;
403	/// Resets all styles after drawing the current glyph
404	/// The only attribute applied after the current glyph
405	const ResetAfter = 0b0000_0010;
406	/// Makes the text bold
407	const Bold = 0b0000_0100;
408	/// Removes bold effect
409	const NoBold = 0b0000_1000;
410	/// Makes the text underlined
411	const Underline = 0b0001_0000;
412	/// Removes underlined effect
413	const NoUnderline = 0b0010_0000;
414	/// Reverse fore- and background color
415	const Reverse = 0b0100_0000;
416	/// Removes underlined effect
417	const NoReverse = 0b1000_0000;
418	}
419}
420
421impl Style {
422	const ATTRIBUTES: [Attribute; 8] = [
423		// ResetBefore
424		Attribute::Reset,
425		// ResetAfter
426		Attribute::Reset,
427		Attribute::Bold,
428		Attribute::NoBold,
429		Attribute::Underlined,
430		Attribute::NoUnderline,
431		Attribute::Reverse,
432		Attribute::NoReverse,
433	];
434
435	#[inline(always)]
436	pub(crate) fn apply_pre_styles(
437		stdout: &mut BufWriter<Stdout>,
438		flags: Style,
439	) -> Result<(), std::io::Error> {
440		for idx in [0, 2, 3, 4, 5, 6, 7] {
441			if flags.bits() & (1 << idx) != 0 {
442				stdout.queue(style::SetAttribute(Self::ATTRIBUTES[idx]))?;
443			}
444		}
445		Ok(())
446	}
447
448	#[inline(always)]
449	pub(crate) fn apply_post_styles(
450		stdout: &mut BufWriter<Stdout>,
451		flags: Style,
452	) -> Result<(), std::io::Error> {
453		const INDEX: u8 = Style::ResetAfter.flag_index();
454		if flags.bits() & (1 << INDEX) != 0 {
455			stdout.queue(style::SetAttribute(Self::ATTRIBUTES[INDEX as usize]))?;
456		}
457		Ok(())
458	}
459
460	#[inline(always)]
461	const fn flag_index(self) -> u8 {
462		let mut n = u8::MIN;
463		while (self.bits() >> (n + 1)) != 0 {
464			n += 1;
465		}
466		n
467	}
468
469	/// Returns self if condition is met otherwise [Self::None]
470	#[inline(always)]
471	pub fn when(self, when: bool) -> Self {
472		Self::from_bits_retain(self.bits() * when as u8)
473	}
474}
475
476// Visual glyph, which consists of a unicode grapheme, its fore- and background color
477#[derive(Debug, Copy, Clone, PartialEq)]
478pub(crate) struct Glyph {
479	pub style: Style,
480	pub bg: Option<Color>,
481	pub fg: Option<Color>,
482	pub grapheme: ArrayString<{ Grapheme::MAX_SIZE }>,
483}
484
485impl Default for Glyph {
486	fn default() -> Self {
487		Self {
488			style: Style::default(),
489			bg: None,
490			fg: None,
491			grapheme: ArrayString::from(Grapheme::PLACEHOLDER.as_str()).unwrap(),
492		}
493	}
494}
495
496impl Glyph {
497	pub const NULL: &'static str = "\0";
498
499	pub fn nullify(&mut self) {
500		self.grapheme.clear();
501		self.grapheme.push_str(Self::NULL);
502	}
503
504	pub fn is_null(&self) -> bool {
505		self.grapheme.as_str() == Self::NULL
506	}
507}
508
509
510pub enum RectError {
511	HorizontalBorderExceeds(Rect, Rect),
512	VerticalBorderExceeds(Rect, Rect),
513}
514
515impl Debug for RectError {
516	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
517		match self {
518			Self::HorizontalBorderExceeds(rect, base) => write!(
519				f,
520				"Rect ({rect:?}) exceeds the base rect's ({base:?}) right border."
521			),
522			Self::VerticalBorderExceeds(rect, base) => write!(
523				f,
524				"Rect ({rect:?}) exceeds the base rect's ({base:?}) right border."
525			),
526		}
527	}
528}
529
530impl Display for RectError {
531	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
532		write!(f, "{self:?}")
533	}
534}
535
536/// Rectangle defining an area
537#[derive(Debug, Copy, Clone, PartialEq)]
538pub struct Rect {
539	pub(crate) start: TPoint,
540	pub(crate) size: TPoint,
541}
542
543impl Rect {
544	/// Creates a new rect which is relative to the base coordinates of {0, 0}
545	/// This will be relative to the base and check their bounds
546	pub const fn subrect(
547		&self,
548		x: TSize,
549		y: TSize,
550		width: TSize,
551		height: TSize,
552	) -> Result<Rect, RectError> {
553		let abs = Rect {
554			start: Vector2D::new(self.start.x + x, self.start.y + y),
555			size: Vector2D::new(width, height),
556		};
557		let base_end = self.end();
558		let end = abs.end();
559		if end.x > base_end.x {
560			return Err(RectError::HorizontalBorderExceeds(*self, abs));
561		}
562		if end.y > base_end.y {
563			return Err(RectError::VerticalBorderExceeds(*self, abs));
564		}
565
566		Ok(abs)
567	}
568
569	/// Same as [Self::subrect] but with [Vector2D]s as arguments
570	pub fn subrect2(&self, offset: TPoint, size: TPoint) -> Result<Rect, RectError> {
571		let abs = Rect {
572			start: self.start + offset,
573			size,
574		};
575		let base_end = self.end();
576		let end = abs.end();
577		if end.x > base_end.x {
578			return Err(RectError::HorizontalBorderExceeds(*self, abs));
579		}
580		if end.y > base_end.y {
581			return Err(RectError::VerticalBorderExceeds(*self, abs));
582		}
583
584		Ok(abs)
585	}
586
587	/// Creates a new Rect with start x and y being 0
588	pub(crate) fn from(width: TSize, height: TSize) -> Self {
589		Self {
590			start: Vector2D::new(TSize::MIN, TSize::MIN),
591			size: Vector2D::new(width, height),
592		}
593	}
594
595	/// Start coordinates
596	pub const fn start(&self) -> TPoint {
597		self.start
598	}
599
600	/// Size of the rectangle
601	pub const fn size(&self) -> TPoint {
602		self.size
603	}
604
605	/// End coordinates (start + end)
606	pub const fn end(&self) -> TPoint {
607		Vector2D::new(self.start.x + self.size.x, self.start.y + self.size.y)
608	}
609
610	/// Checks if the two rects overlap
611	pub fn overlaps(&self, other: &Self) -> bool {
612		let end1 = self.end();
613		let end2 = other.end();
614		let x_overlap = self.start.x < end2.x
615			&& end1.x > other.start.x
616			&& self.start.y < end2.y
617			&& end1.y > other.start.y;
618		let y_overlap = self.start.x < end2.x
619			&& end1.x > other.start.x
620			&& self.start.y > end2.y
621			&& end1.y < other.start.y;
622		x_overlap || y_overlap
623	}
624
625	/// Splits the rect into two seperate rects at the given `orientation`'s line
626	/// `ratio` determines the size of the first rect, the other rect will occupy the remaining
627	/// size
628	/// 'padding' might be adjusted towards 0 in case one of the rect's area would be 0 => Thus
629	/// this method will always produce rects with a width/height of 1
630	pub fn split(
631		&self,
632		orientation: Orientation,
633		ratio: Percent,
634		mut padding: TSize,
635	) -> Result<SplitRect, RectError> {
636		let min_base_base = self.size[orientation.invert() as usize].saturating_sub(2);
637		if padding >= min_base_base {
638			padding = min_base_base;
639		}
640		eprintln!("padding: {padding}");
641
642		match orientation {
643			Orientation::Horizontal => {
644				let y = self.size().y - padding;
645				let subsize = (ratio.value() as TSize * y).div_ceil(100);
646				let padding_area = match padding {
647					0 => None,
648					v => Some(self.subrect(0, subsize, self.size().x, v)?),
649				};
650
651				Ok(SplitRect {
652					rects: (
653						self.subrect(0, 0, self.size().x, subsize)?,
654						self.subrect(0, subsize + padding, self.size().x, y - subsize)?,
655					),
656					padding_area,
657				})
658			}
659			Orientation::Vertical => {
660				let x = self.size().x - padding;
661				let subsize = (ratio.value() as TSize * x).div_ceil(100);
662				let padding_area = match padding {
663					0 => None,
664					v => Some(self.subrect(subsize, 0, v, self.size().y)?),
665				};
666				Ok(SplitRect {
667					rects: (
668						self.subrect(0, 0, subsize, self.size().y)?,
669						self.subrect(subsize + padding, 0, x - subsize, self.size().y)?,
670					),
671					padding_area,
672				})
673			}
674		}
675	}
676
677
678	/// Returns the inner area
679	pub const fn area(&self) -> TSize {
680		self.size.x * self.size.y
681	}
682}
683
684#[derive(Debug)]
685pub struct SplitRect {
686	pub rects: (Rect, Rect),
687	/// None if padding is 0
688	pub padding_area: Option<Rect>,
689}
690
691#[cfg(test)]
692mod test {
693	use super::*;
694
695	#[test]
696	fn grapheme_invalid_chars() {
697		for c in 0..32 {
698			assert_eq!(
699				Grapheme::from(&char::from_u32(c).unwrap().to_string()),
700				Err(GraphemeError::InvalidGlyphWidth)
701			);
702		}
703	}
704
705	#[test]
706	fn grapheme_check_count() {
707		assert_eq!(Grapheme::from("abc"), Err(GraphemeError::TooManyGraphemes));
708		assert_eq!(Grapheme::from("ʤĵ"), Err(GraphemeError::TooManyGraphemes));
709	}
710
711	#[test]
712	fn grapheme_check_size() {
713		assert_eq!(Grapheme::from("🤦🏻‍♂️"), Err(GraphemeError::GraphemeTooBig));
714	}
715
716	#[test]
717	fn percent_int() {
718		assert_eq!(Percent::from_int(0), Percent::from_int(1));
719		assert_eq!(Percent::from_int(101), Percent::from_int(100));
720		for v in 1..=100 {
721			assert_eq!(Percent::from_int(v).value(), v);
722		}
723	}
724
725	#[test]
726	fn percent_float() {
727		assert_eq!(Percent::from_float(0.001), Percent::from_int(1));
728		assert_eq!(Percent::from_float(1.1), Percent::from_int(100));
729		assert_eq!(Percent::from_float(f32::INFINITY), Percent::from_int(100));
730		let mut v = 0.01;
731		while v <= 1. {
732			assert_eq!(Percent::from_float(v).value(), (v * 100.) as u8);
733			v += 0.01;
734		}
735	}
736
737	mod line {
738		use super::*;
739
740		#[test]
741		fn check_ctor() {
742			assert_eq!(
743				Line2D::from(Vector2D::new(0, 4), Vector2D::new(2, 1)),
744				Line2D::new(0, 4, 2, 1)
745			);
746		}
747
748		#[test]
749		fn check_iter() {
750			let f1 = |x: TSize| 2 * x + 4;
751			let l1 = Line2D::new(0, 4, 16, 4);
752			let l2 = Line2D::new(2, 4, 2, 20);
753			let l3 = Line2D::new(0, f1(0), 12, f1(12));
754
755			for (v, exp) in l1.iter_points().zip(0..16) {
756				assert_eq!(v.x, exp);
757			}
758
759			for (v, exp) in l2.iter_points().zip(4..20) {
760				assert_eq!(v.y, exp);
761			}
762
763			for v in l3.iter_points() {
764				assert_eq!(v.y, f1(v.x));
765			}
766		}
767
768		#[test]
769		fn check_linear_properties() {
770			let l1 = Line2D::new(0, 4, 2, 1);
771			let l2 = Line2D::new(2, 1, 4, 5);
772			assert!(l1.is_descending());
773			assert!(l2.is_ascending());
774			assert!(!l1.is_vertical());
775			assert!(!l2.is_horizontal());
776
777			let l3 = Line2D::new(2, 4, 2, 12);
778			let l4 = Line2D::new(1, 2, 12, 2);
779			assert!(l3.is_vertical());
780			assert!(l4.is_horizontal());
781			assert!(l4.is_constant());
782		}
783	}
784
785	#[test]
786	fn rect_check() {
787		let rect = Rect::from(100, 100);
788		let subrect = rect.subrect(20, 20, 50, 50).unwrap();
789		assert_ne!(rect.start(), subrect.start());
790		assert_ne!(rect.end(), subrect.end());
791		assert_ne!(rect.area(), subrect.area());
792		assert!(subrect.overlaps(&rect));
793		assert!(!subrect.overlaps(&Rect::from(20, 20)));
794		assert!(subrect.overlaps(&Rect::from(21, 21)));
795		assert!(!subrect.overlaps(&Rect::from(20, 21)));
796		assert!(!subrect.overlaps(&Rect::from(21, 20)));
797		assert!(!subrect.overlaps(&rect.subrect(70, 70, 20, 20).unwrap()));
798		assert!(subrect.overlaps(&rect.subrect(69, 69, 20, 20).unwrap()));
799		assert!(!subrect.overlaps(&rect.subrect(70, 69, 20, 20).unwrap()));
800		assert!(!subrect.overlaps(&rect.subrect(69, 70, 20, 20).unwrap()));
801		let subsubrect = subrect.subrect(10, 10, 20, 20).unwrap();
802		assert_eq!(subsubrect.start(), subrect.start() + Vector2D::new(10, 10));
803		assert!(
804			subrect
805				.subrect(0, 0, subrect.size().x + 1, subrect.size().y + 1)
806				.is_err()
807		);
808	}
809
810	#[test]
811	fn rect_split_check() {
812		let rect = Rect::from(64, 64);
813		let p = Percent::from_int(20);
814		let split = rect.split(Orientation::Horizontal, p, 2).unwrap();
815		let expected_rect_0 = Rect {
816			start: Vector2D::new(0, 0),
817			size: Vector2D::new(64, 13),
818		};
819		let expected_rect_1 = Rect {
820			start: Vector2D::new(0, 15),
821			size: Vector2D::new(64, 49),
822		};
823		assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
824
825		let p = Percent::from_int(40);
826		let split = rect.split(Orientation::Vertical, p, 3).unwrap();
827		let expected_rect_0 = Rect {
828			start: Vector2D::new(0, 0),
829			size: Vector2D::new(25, 64),
830		};
831		let expected_rect_1 = Rect {
832			start: Vector2D::new(28, 0),
833			size: Vector2D::new(36, 64),
834		};
835		assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
836
837
838		let small_rect = Rect::from(4, 4);
839		let p = Percent::from_int(20);
840		let split = small_rect.split(Orientation::Horizontal, p, 2).unwrap();
841		let expected_rect_0 = Rect {
842			start: Vector2D::new(0, 0),
843			size: Vector2D::new(4, 1),
844		};
845		let expected_rect_1 = Rect {
846			start: Vector2D::new(0, 3),
847			size: Vector2D::new(4, 1),
848		};
849		assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
850
851		let split = small_rect.split(Orientation::Horizontal, p, 4).unwrap();
852		assert_eq!(split.rects, (expected_rect_0, expected_rect_1));
853	}
854}