1use libm::{cosf, sinf};
9use rlvgl_core::event::Event;
10use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
11use rlvgl_core::raster::PointF;
12use rlvgl_core::renderer::Renderer;
13use rlvgl_core::style::Style;
14use rlvgl_core::widget::{Color, Rect, Widget};
15
16const DEFAULT_TICK_COUNT: u16 = 6;
17const DEFAULT_MAJOR_TICK_EVERY: u16 = 1;
18const DEFAULT_ANGLE_RANGE: i32 = 270;
19const DEFAULT_ROTATION: i32 = 135;
20const MINOR_TICK_LENGTH: i32 = 5;
21const MAJOR_TICK_LENGTH: i32 = 9;
22const LABEL_GAP: i32 = 3;
23const LABEL_CHAR_WIDTH: i32 = 6;
24const LABEL_BASELINE_HALF: i32 = 4;
25const LABEL_BUF_LEN: usize = 12;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum ScaleMode {
30 HorizontalBottom,
32 HorizontalTop,
34 VerticalLeft,
36 VerticalRight,
38 RoundInner,
40 RoundOuter,
42}
43
44pub struct Scale {
50 bounds: Rect,
51 min: i32,
52 max: i32,
53 tick_count: u16,
54 major_tick_every: u16,
55 label_show: bool,
56 mode: ScaleMode,
57 angle_range: i32,
58 rotation: i32,
59 pub style: Style,
63 pub major_tick_color: Color,
65 pub minor_tick_color: Color,
67 pub label_color: Color,
69 font: WidgetFont,
72}
73
74#[derive(Clone, Copy)]
75struct RoundLabelAnchor {
76 center: PointF,
77 cos_t: f32,
78 sin_t: f32,
79 tick_end_radius: f32,
80 major: bool,
81}
82
83impl Scale {
84 pub fn new(bounds: Rect) -> Self {
89 Self::with_range(bounds, 0, 100)
90 }
91
92 pub fn with_range(bounds: Rect, min: i32, max: i32) -> Self {
94 let style = Style {
95 border_width: 1,
96 ..Style::default()
97 };
98
99 Self {
100 bounds,
101 min,
102 max,
103 tick_count: DEFAULT_TICK_COUNT,
104 major_tick_every: DEFAULT_MAJOR_TICK_EVERY,
105 label_show: true,
106 mode: ScaleMode::HorizontalBottom,
107 angle_range: DEFAULT_ANGLE_RANGE,
108 rotation: DEFAULT_ROTATION,
109 style,
110 major_tick_color: Color(0, 0, 0, 255),
111 minor_tick_color: Color(96, 96, 96, 255),
112 label_color: Color(0, 0, 0, 255),
113 font: WidgetFont::new(),
114 }
115 }
116
117 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
120 self.font.set(font);
121 }
122
123 pub fn set_mode(&mut self, mode: ScaleMode) {
125 self.mode = mode;
126 }
127
128 pub fn mode(&self) -> ScaleMode {
130 self.mode
131 }
132
133 pub fn set_range(&mut self, min: i32, max: i32) {
138 self.min = min;
139 self.max = max;
140 }
141
142 pub fn min_value(&self) -> i32 {
144 self.min
145 }
146
147 pub fn max_value(&self) -> i32 {
149 self.max
150 }
151
152 pub fn set_tick_count(&mut self, tick_count: u16) {
156 self.tick_count = tick_count;
157 }
158
159 pub fn tick_count(&self) -> u16 {
161 self.tick_count
162 }
163
164 pub fn set_total_tick_count(&mut self, tick_count: u16) {
166 self.set_tick_count(tick_count);
167 }
168
169 pub fn total_tick_count(&self) -> u16 {
171 self.tick_count()
172 }
173
174 pub fn set_major_tick_every(&mut self, every: u16) {
179 self.major_tick_every = every;
180 }
181
182 pub fn major_tick_every(&self) -> u16 {
184 self.major_tick_every
185 }
186
187 pub fn set_label_visible(&mut self, visible: bool) {
189 self.label_show = visible;
190 }
191
192 pub fn label_visible(&self) -> bool {
194 self.label_show
195 }
196
197 pub fn set_label_show(&mut self, show: bool) {
199 self.set_label_visible(show);
200 }
201
202 pub fn label_show(&self) -> bool {
204 self.label_visible()
205 }
206
207 pub fn set_angle_range(&mut self, degrees: i32) {
209 self.angle_range = degrees.clamp(0, 360);
210 }
211
212 pub fn angle_range(&self) -> i32 {
214 self.angle_range
215 }
216
217 pub fn set_rotation(&mut self, degrees: i32) {
219 self.rotation = normalize_turn(degrees);
220 }
221
222 pub fn rotation(&self) -> i32 {
224 self.rotation
225 }
226
227 fn draw_linear(&self, renderer: &mut dyn Renderer) {
228 let Some((axis_start, axis_end, positive_side)) = self.linear_axis() else {
229 return;
230 };
231 let axis_color = self.style.border_color.with_alpha(self.style.alpha);
232 if let Some(width) = self.stroke_width()
233 && axis_color.3 != 0
234 {
235 renderer.stroke_line_aa(axis_start, axis_end, width, axis_color);
236 }
237
238 for index in 0..usize::from(self.tick_count) {
239 let major = self.is_major_tick(index);
240 let point = self.linear_tick_point(index);
241 let length = self.tick_length(major);
242 let tick_end = match self.mode {
243 ScaleMode::HorizontalBottom | ScaleMode::HorizontalTop => {
244 PointF::new(point.x, point.y + positive_side * length as f32)
245 }
246 ScaleMode::VerticalLeft | ScaleMode::VerticalRight => {
247 PointF::new(point.x + positive_side * length as f32, point.y)
248 }
249 ScaleMode::RoundInner | ScaleMode::RoundOuter => point,
250 };
251 self.draw_tick(renderer, point, tick_end, major);
252 self.draw_label(renderer, index, point, positive_side, major);
253 }
254 }
255
256 fn draw_round(&self, renderer: &mut dyn Renderer) {
257 let Some((center, axis_radius)) = self.round_center_and_axis_radius() else {
258 return;
259 };
260 self.draw_round_axis(renderer, center, axis_radius);
261
262 for index in 0..usize::from(self.tick_count) {
263 let major = self.is_major_tick(index);
264 let length = self.tick_length(major) as f32;
265 let angle = self.tick_angle(index);
266 let (cos_t, sin_t) = angle_vector(angle);
267 let (from_r, to_r) = match self.mode {
268 ScaleMode::RoundInner => (axis_radius, (axis_radius - length).max(0.0)),
269 ScaleMode::RoundOuter => (axis_radius, axis_radius + length),
270 _ => (axis_radius, axis_radius),
271 };
272 let from = polar(center, cos_t, sin_t, from_r);
273 let to = polar(center, cos_t, sin_t, to_r);
274 self.draw_tick(renderer, from, to, major);
275 self.draw_round_label(
276 renderer,
277 index,
278 RoundLabelAnchor {
279 center,
280 cos_t,
281 sin_t,
282 tick_end_radius: to_r,
283 major,
284 },
285 );
286 }
287 }
288
289 fn draw_round_axis(&self, renderer: &mut dyn Renderer, center: PointF, axis_radius: f32) {
290 let Some(width) = self.stroke_width() else {
291 return;
292 };
293 let color = self.style.border_color.with_alpha(self.style.alpha);
294 if color.3 == 0 || self.angle_range == 0 || axis_radius <= 0.0 {
295 return;
296 }
297
298 let half_width = width * 0.5;
299 let r_outer = axis_radius + half_width;
300 let r_inner = (axis_radius - half_width).max(0.0);
301 let (start_cos, start_sin) = angle_vector(self.rotation);
302 let (end_cos, end_sin) = angle_vector(self.rotation + self.angle_range);
303 let extent = self.angle_range as f32 * core::f32::consts::PI / 180.0;
304 renderer.fill_arc_aa(
305 center, r_outer, r_inner, start_cos, start_sin, end_cos, end_sin, extent, color,
306 );
307 }
308
309 fn draw_tick(&self, renderer: &mut dyn Renderer, from: PointF, to: PointF, major: bool) {
310 let Some(width) = self.stroke_width() else {
311 return;
312 };
313 let color = if major {
314 self.major_tick_color
315 } else {
316 self.minor_tick_color
317 }
318 .with_alpha(self.style.alpha);
319 if color.3 != 0 {
320 renderer.stroke_line_aa(from, to, width, color);
321 }
322 }
323
324 fn draw_label(
325 &self,
326 renderer: &mut dyn Renderer,
327 index: usize,
328 tick_point: PointF,
329 positive_side: f32,
330 major: bool,
331 ) {
332 if !self.label_show || !major {
333 return;
334 }
335
336 let label = LabelBuf::new(self.tick_value(index));
337 let text_x = match self.mode {
338 ScaleMode::HorizontalBottom | ScaleMode::HorizontalTop => {
339 tick_point.x as i32 - label.pixel_width() / 2
340 }
341 ScaleMode::VerticalRight => tick_point.x as i32 + MAJOR_TICK_LENGTH + LABEL_GAP,
342 ScaleMode::VerticalLeft => {
343 tick_point.x as i32 - MAJOR_TICK_LENGTH - LABEL_GAP - label.pixel_width()
344 }
345 ScaleMode::RoundInner | ScaleMode::RoundOuter => tick_point.x as i32,
346 };
347 let text_y = match self.mode {
348 ScaleMode::HorizontalBottom => {
349 tick_point.y as i32 + MAJOR_TICK_LENGTH + LABEL_GAP + LABEL_BASELINE_HALF
350 }
351 ScaleMode::HorizontalTop => {
352 tick_point.y as i32 - MAJOR_TICK_LENGTH - LABEL_GAP - LABEL_BASELINE_HALF
353 }
354 ScaleMode::VerticalLeft | ScaleMode::VerticalRight => {
355 tick_point.y as i32 + LABEL_BASELINE_HALF
356 }
357 ScaleMode::RoundInner | ScaleMode::RoundOuter => tick_point.y as i32,
358 };
359
360 let _ = positive_side;
361 self.draw_label_text(renderer, label.as_str(), (text_x, text_y));
362 }
363
364 fn draw_round_label(
365 &self,
366 renderer: &mut dyn Renderer,
367 index: usize,
368 anchor: RoundLabelAnchor,
369 ) {
370 if !self.label_show || !anchor.major {
371 return;
372 }
373
374 let label = LabelBuf::new(self.tick_value(index));
375 let label_radius = match self.mode {
376 ScaleMode::RoundInner => (anchor.tick_end_radius - LABEL_GAP as f32).max(0.0),
377 ScaleMode::RoundOuter => anchor.tick_end_radius + LABEL_GAP as f32,
378 _ => anchor.tick_end_radius,
379 };
380 let p = polar(anchor.center, anchor.cos_t, anchor.sin_t, label_radius);
381 self.draw_label_text(
382 renderer,
383 label.as_str(),
384 (
385 p.x as i32 - label.pixel_width() / 2,
386 p.y as i32 + LABEL_BASELINE_HALF,
387 ),
388 );
389 }
390
391 fn draw_label_text(&self, renderer: &mut dyn Renderer, text: &str, origin: (i32, i32)) {
392 let font = self.font.resolve();
393 let shaped = shape_text_ltr(font, text, origin, 0);
394 renderer.draw_text_shaped(
395 &shaped,
396 (0, 0),
397 self.label_color.with_alpha(self.style.alpha),
398 );
399 }
400
401 fn linear_axis(&self) -> Option<(PointF, PointF, f32)> {
402 match self.mode {
403 ScaleMode::HorizontalBottom => Some((
404 PointF::new(self.bounds.x as f32, self.bounds.y as f32),
405 PointF::new(
406 (self.bounds.x + self.bounds.width) as f32,
407 self.bounds.y as f32,
408 ),
409 1.0,
410 )),
411 ScaleMode::HorizontalTop => Some((
412 PointF::new(
413 self.bounds.x as f32,
414 (self.bounds.y + self.bounds.height) as f32,
415 ),
416 PointF::new(
417 (self.bounds.x + self.bounds.width) as f32,
418 (self.bounds.y + self.bounds.height) as f32,
419 ),
420 -1.0,
421 )),
422 ScaleMode::VerticalRight => Some((
423 PointF::new(self.bounds.x as f32, self.bounds.y as f32),
424 PointF::new(
425 self.bounds.x as f32,
426 (self.bounds.y + self.bounds.height) as f32,
427 ),
428 1.0,
429 )),
430 ScaleMode::VerticalLeft => Some((
431 PointF::new(
432 (self.bounds.x + self.bounds.width) as f32,
433 self.bounds.y as f32,
434 ),
435 PointF::new(
436 (self.bounds.x + self.bounds.width) as f32,
437 (self.bounds.y + self.bounds.height) as f32,
438 ),
439 -1.0,
440 )),
441 ScaleMode::RoundInner | ScaleMode::RoundOuter => None,
442 }
443 }
444
445 fn linear_tick_point(&self, index: usize) -> PointF {
446 match self.mode {
447 ScaleMode::HorizontalBottom => PointF::new(
448 self.axis_position(self.bounds.x, self.bounds.width, index) as f32,
449 self.bounds.y as f32,
450 ),
451 ScaleMode::HorizontalTop => PointF::new(
452 self.axis_position(self.bounds.x, self.bounds.width, index) as f32,
453 (self.bounds.y + self.bounds.height) as f32,
454 ),
455 ScaleMode::VerticalRight => PointF::new(
456 self.bounds.x as f32,
457 self.axis_position(self.bounds.y, self.bounds.height, index) as f32,
458 ),
459 ScaleMode::VerticalLeft => PointF::new(
460 (self.bounds.x + self.bounds.width) as f32,
461 self.axis_position(self.bounds.y, self.bounds.height, index) as f32,
462 ),
463 ScaleMode::RoundInner | ScaleMode::RoundOuter => PointF::new(0.0, 0.0),
464 }
465 }
466
467 fn round_center_and_axis_radius(&self) -> Option<(PointF, f32)> {
468 let radius = self.bounds.width.min(self.bounds.height) as f32 * 0.5;
469 if radius <= 0.0 {
470 return None;
471 }
472
473 let width = self.stroke_width().unwrap_or(1.0);
474 let axis_radius = match self.mode {
475 ScaleMode::RoundInner => radius - width,
476 ScaleMode::RoundOuter => radius - MAJOR_TICK_LENGTH as f32 - width,
477 _ => radius,
478 };
479 if axis_radius <= 0.0 {
480 return None;
481 }
482
483 Some((
484 PointF::new(
485 self.bounds.x as f32 + self.bounds.width as f32 * 0.5,
486 self.bounds.y as f32 + self.bounds.height as f32 * 0.5,
487 ),
488 axis_radius,
489 ))
490 }
491
492 fn axis_position(&self, origin: i32, length: i32, index: usize) -> i32 {
493 let count = usize::from(self.tick_count);
494 if count <= 1 {
495 return origin;
496 }
497 let den = (count - 1) as i64;
498 (i64::from(origin) + i64::from(length) * index as i64 / den) as i32
499 }
500
501 fn tick_value(&self, index: usize) -> i32 {
502 let count = usize::from(self.tick_count);
503 if count <= 1 {
504 return self.min;
505 }
506 let den = (count - 1) as i64;
507 let value =
508 i64::from(self.min) + (i64::from(self.max) - i64::from(self.min)) * index as i64 / den;
509 value as i32
510 }
511
512 fn tick_angle(&self, index: usize) -> i32 {
513 let count = usize::from(self.tick_count);
514 if count <= 1 {
515 return self.rotation;
516 }
517 let den = (count - 1) as i64;
518 self.rotation + (i64::from(self.angle_range) * index as i64 / den) as i32
519 }
520
521 fn is_major_tick(&self, index: usize) -> bool {
522 self.major_tick_every != 0 && index.is_multiple_of(usize::from(self.major_tick_every))
523 }
524
525 fn tick_length(&self, major: bool) -> i32 {
526 if major {
527 MAJOR_TICK_LENGTH
528 } else {
529 MINOR_TICK_LENGTH
530 }
531 }
532
533 fn stroke_width(&self) -> Option<f32> {
534 (self.style.border_width > 0).then_some(self.style.border_width as f32)
535 }
536}
537
538impl Widget for Scale {
539 fn bounds(&self) -> Rect {
540 self.bounds
541 }
542
543 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
544 Some(&mut self.font)
545 }
546
547 fn draw(&self, renderer: &mut dyn Renderer) {
548 if self.bounds.width <= 0 || self.bounds.height <= 0 || self.style.alpha == 0 {
549 return;
550 }
551
552 match self.mode {
553 ScaleMode::RoundInner | ScaleMode::RoundOuter => self.draw_round(renderer),
554 ScaleMode::HorizontalBottom
555 | ScaleMode::HorizontalTop
556 | ScaleMode::VerticalLeft
557 | ScaleMode::VerticalRight => self.draw_linear(renderer),
558 }
559 }
560
561 fn handle_event(&mut self, _event: &Event) -> bool {
562 false
563 }
564
565 fn set_bounds(&mut self, bounds: Rect) {
566 self.bounds = bounds;
567 }
568}
569
570struct LabelBuf {
571 bytes: [u8; LABEL_BUF_LEN],
572 start: usize,
573}
574
575impl LabelBuf {
576 fn new(value: i32) -> Self {
577 let mut bytes = [0; LABEL_BUF_LEN];
578 let mut cursor = LABEL_BUF_LEN;
579 let mut n = i64::from(value);
580 let negative = n < 0;
581 if negative {
582 n = -n;
583 }
584
585 loop {
586 cursor -= 1;
587 bytes[cursor] = b'0' + (n % 10) as u8;
588 n /= 10;
589 if n == 0 {
590 break;
591 }
592 }
593
594 if negative {
595 cursor -= 1;
596 bytes[cursor] = b'-';
597 }
598
599 Self {
600 bytes,
601 start: cursor,
602 }
603 }
604
605 fn as_str(&self) -> &str {
606 core::str::from_utf8(&self.bytes[self.start..]).unwrap_or("")
607 }
608
609 fn pixel_width(&self) -> i32 {
610 (LABEL_BUF_LEN - self.start) as i32 * LABEL_CHAR_WIDTH
611 }
612}
613
614fn normalize_turn(degrees: i32) -> i32 {
615 degrees.rem_euclid(360)
616}
617
618fn angle_vector(degrees: i32) -> (f32, f32) {
619 let radians = normalize_turn(degrees) as f32 * core::f32::consts::PI / 180.0;
620 (cosf(radians), sinf(radians))
621}
622
623fn polar(center: PointF, cos_t: f32, sin_t: f32, radius: f32) -> PointF {
624 PointF::new(center.x + cos_t * radius, center.y + sin_t * radius)
625}
626
627#[cfg(test)]
628mod tests {
629 extern crate alloc;
630
631 use super::*;
632 use alloc::string::String;
633 use alloc::vec::Vec;
634 use rlvgl_core::font::ShapedText;
635
636 #[derive(Clone, Copy, Debug, PartialEq)]
637 struct Stroke {
638 from: PointF,
639 to: PointF,
640 width: f32,
641 color: Color,
642 }
643
644 #[derive(Clone, Debug, PartialEq, Eq)]
645 struct Text {
646 position: (i32, i32),
647 content: String,
648 color: Color,
649 }
650
651 struct RecordingRenderer {
652 strokes: Vec<Stroke>,
653 texts: Vec<Text>,
654 arcs: usize,
655 }
656
657 impl RecordingRenderer {
658 fn new() -> Self {
659 Self {
660 strokes: Vec::new(),
661 texts: Vec::new(),
662 arcs: 0,
663 }
664 }
665
666 fn strokes_with_color(&self, color: Color) -> Vec<Stroke> {
667 self.strokes
668 .iter()
669 .copied()
670 .filter(|stroke| stroke.color == color)
671 .collect()
672 }
673 }
674
675 impl Renderer for RecordingRenderer {
676 fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
677
678 fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
679 self.texts.push(Text {
680 position,
681 content: String::from(text),
682 color,
683 });
684 }
685
686 fn draw_text_shaped(&mut self, shaped: &ShapedText<'_>, _origin: (i32, i32), color: Color) {
687 self.texts.push(Text {
688 position: (shaped.bounds.x, shaped.bounds.y),
689 content: shaped.glyphs.iter().map(|glyph| glyph.ch).collect(),
690 color,
691 });
692 }
693
694 #[allow(clippy::too_many_arguments)]
695 fn fill_arc_aa(
696 &mut self,
697 _center: PointF,
698 _r_outer: f32,
699 _r_inner: f32,
700 _start_cos: f32,
701 _start_sin: f32,
702 _end_cos: f32,
703 _end_sin: f32,
704 _extent: f32,
705 _color: Color,
706 ) {
707 self.arcs += 1;
708 }
709
710 fn stroke_line_aa(&mut self, from: PointF, to: PointF, width: f32, color: Color) {
711 self.strokes.push(Stroke {
712 from,
713 to,
714 width,
715 color,
716 });
717 }
718 }
719
720 #[test]
721 fn linear_tick_classification_splits_major_and_minor() {
722 let mut scale = Scale::new(rect(10, 20, 100, 40));
723 scale.set_range(0, 100);
724 scale.set_tick_count(5);
725 scale.set_major_tick_every(2);
726 scale.set_label_visible(false);
727 scale.style.border_color = Color(1, 2, 3, 255);
728 scale.style.border_width = 2;
729 scale.major_tick_color = Color(10, 20, 30, 255);
730 scale.minor_tick_color = Color(40, 50, 60, 255);
731
732 let mut renderer = RecordingRenderer::new();
733 scale.draw(&mut renderer);
734
735 let major = renderer.strokes_with_color(scale.major_tick_color);
736 let minor = renderer.strokes_with_color(scale.minor_tick_color);
737 assert_eq!(major.len(), 3);
738 assert_eq!(minor.len(), 2);
739 assert_eq!(
740 major[0],
741 Stroke {
742 from: PointF::new(10.0, 20.0),
743 to: PointF::new(10.0, 29.0),
744 width: 2.0,
745 color: scale.major_tick_color,
746 }
747 );
748 assert_eq!(
749 minor[0],
750 Stroke {
751 from: PointF::new(35.0, 20.0),
752 to: PointF::new(35.0, 25.0),
753 width: 2.0,
754 color: scale.minor_tick_color,
755 }
756 );
757 assert!(renderer.texts.is_empty());
758 }
759
760 #[test]
761 fn linear_modes_place_ticks_on_the_configured_side() {
762 let cases = [
763 (
764 ScaleMode::HorizontalBottom,
765 PointF::new(0.0, 0.0),
766 PointF::new(0.0, 9.0),
767 ),
768 (
769 ScaleMode::HorizontalTop,
770 PointF::new(0.0, 50.0),
771 PointF::new(0.0, 41.0),
772 ),
773 (
774 ScaleMode::VerticalRight,
775 PointF::new(0.0, 0.0),
776 PointF::new(9.0, 0.0),
777 ),
778 (
779 ScaleMode::VerticalLeft,
780 PointF::new(100.0, 0.0),
781 PointF::new(91.0, 0.0),
782 ),
783 ];
784
785 for (mode, from, to) in cases {
786 let mut scale = Scale::new(rect(0, 0, 100, 50));
787 scale.set_range(0, 10);
788 scale.set_mode(mode);
789 scale.set_tick_count(2);
790 scale.set_label_visible(false);
791 scale.major_tick_color = Color(11, 22, 33, 255);
792
793 let mut renderer = RecordingRenderer::new();
794 scale.draw(&mut renderer);
795
796 let major = renderer.strokes_with_color(scale.major_tick_color);
797 assert_eq!(major[0].from, from);
798 assert_eq!(major[0].to, to);
799 }
800 }
801
802 #[test]
803 fn labels_follow_major_ticks_and_can_be_hidden() {
804 let mut scale = Scale::new(rect(0, 0, 120, 30));
805 scale.set_range(-10, 10);
806 scale.set_tick_count(3);
807 scale.set_major_tick_every(1);
808 scale.major_tick_color = Color(17, 18, 19, 255);
809 scale.label_color = Color(7, 8, 9, 255);
810
811 let mut renderer = RecordingRenderer::new();
812 scale.draw(&mut renderer);
813 assert_eq!(renderer.texts.len(), 3);
814 assert_eq!(renderer.texts[0].content, "-10");
815 assert_eq!(renderer.texts[1].content, "0");
816 assert_eq!(renderer.texts[2].content, "10");
817
818 scale.set_label_visible(false);
819 let mut hidden = RecordingRenderer::new();
820 scale.draw(&mut hidden);
821 assert!(hidden.texts.is_empty());
822 assert_eq!(hidden.strokes_with_color(scale.major_tick_color).len(), 3);
823 }
824
825 #[test]
826 fn major_every_zero_disables_major_ticks_and_labels() {
827 let mut scale = Scale::new(rect(0, 0, 90, 30));
828 scale.set_range(0, 30);
829 scale.set_tick_count(4);
830 scale.set_major_tick_every(0);
831 scale.set_label_visible(true);
832 scale.major_tick_color = Color(1, 1, 1, 255);
833 scale.minor_tick_color = Color(2, 2, 2, 255);
834
835 let mut renderer = RecordingRenderer::new();
836 scale.draw(&mut renderer);
837
838 assert!(renderer.texts.is_empty());
839 assert!(
840 renderer
841 .strokes_with_color(scale.major_tick_color)
842 .is_empty()
843 );
844 assert_eq!(renderer.strokes_with_color(scale.minor_tick_color).len(), 4);
845 }
846
847 #[test]
848 fn round_modes_draw_ticks_on_inner_and_outer_radial_sides() {
849 let mut outer = Scale::new(rect(0, 0, 100, 100));
850 outer.set_range(0, 10);
851 outer.set_mode(ScaleMode::RoundOuter);
852 outer.set_rotation(0);
853 outer.set_angle_range(90);
854 outer.set_tick_count(2);
855 outer.set_label_visible(false);
856 outer.major_tick_color = Color(3, 4, 5, 255);
857
858 let mut outer_renderer = RecordingRenderer::new();
859 outer.draw(&mut outer_renderer);
860 let outer_major = outer_renderer.strokes_with_color(outer.major_tick_color);
861 assert_eq!(outer_renderer.arcs, 1);
862 assert_eq!(outer_major[0].from, PointF::new(90.0, 50.0));
863 assert_eq!(outer_major[0].to, PointF::new(99.0, 50.0));
864
865 let mut inner = outer;
866 inner.set_mode(ScaleMode::RoundInner);
867 let mut inner_renderer = RecordingRenderer::new();
868 inner.draw(&mut inner_renderer);
869 let inner_major = inner_renderer.strokes_with_color(inner.major_tick_color);
870 assert_eq!(inner_renderer.arcs, 1);
871 assert_eq!(inner_major[0].from, PointF::new(99.0, 50.0));
872 assert_eq!(inner_major[0].to, PointF::new(90.0, 50.0));
873 }
874
875 #[test]
876 fn set_bounds_adopts_layout_bounds() {
877 let mut scale = Scale::new(rect(1, 2, 3, 4));
878 scale.set_range(0, 1);
879
880 scale.set_bounds(rect(10, 11, 120, 32));
881
882 assert_eq!(scale.bounds(), rect(10, 11, 120, 32));
883 }
884
885 fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
886 Rect {
887 x,
888 y,
889 width,
890 height,
891 }
892 }
893}