1use crate::console::{Console, PixelsXY};
19use async_trait::async_trait;
20use endbasic_core::ast::{ArgSep, ExprType};
21use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax};
22use endbasic_core::exec::{Machine, Scope};
23use endbasic_core::syms::{
24 CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
25};
26use endbasic_core::LineCol;
27use std::borrow::Cow;
28use std::cell::RefCell;
29use std::convert::TryFrom;
30use std::rc::Rc;
31
32const CATEGORY: &str = "Graphics
34The EndBASIC console overlays text and graphics in the same canvas. The consequence of this \
35design choice is that the console has two coordinate systems: the character-based system, used by
36the commands described in HELP \"CONSOLE\", and the pixel-based system, used by the commands \
37described in this section.";
38
39fn parse_coordinate(i: i32, pos: LineCol) -> Result<i16, CallError> {
41 match i16::try_from(i) {
42 Ok(i) => Ok(i),
43 Err(_) => Err(CallError::ArgumentError(pos, format!("Coordinate {} out of range", i))),
44 }
45}
46
47fn parse_coordinates(
49 xvalue: i32,
50 xpos: LineCol,
51 yvalue: i32,
52 ypos: LineCol,
53) -> Result<PixelsXY, CallError> {
54 Ok(PixelsXY { x: parse_coordinate(xvalue, xpos)?, y: parse_coordinate(yvalue, ypos)? })
55}
56
57fn parse_radius(i: i32, pos: LineCol) -> Result<u16, CallError> {
59 match u16::try_from(i) {
60 Ok(i) => Ok(i),
61 Err(_) if i < 0 => {
62 Err(CallError::ArgumentError(pos, format!("Radius {} must be positive", i)))
63 }
64 Err(_) => Err(CallError::ArgumentError(pos, format!("Radius {} out of range", i))),
65 }
66}
67
68pub struct GfxCircleCommand {
70 metadata: CallableMetadata,
71 console: Rc<RefCell<dyn Console>>,
72}
73
74impl GfxCircleCommand {
75 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
77 Rc::from(Self {
78 metadata: CallableMetadataBuilder::new("GFX_CIRCLE")
79 .with_syntax(&[(
80 &[
81 SingularArgSyntax::RequiredValue(
82 RequiredValueSyntax {
83 name: Cow::Borrowed("x"),
84 vtype: ExprType::Integer,
85 },
86 ArgSepSyntax::Exactly(ArgSep::Long),
87 ),
88 SingularArgSyntax::RequiredValue(
89 RequiredValueSyntax {
90 name: Cow::Borrowed("y"),
91 vtype: ExprType::Integer,
92 },
93 ArgSepSyntax::Exactly(ArgSep::Long),
94 ),
95 SingularArgSyntax::RequiredValue(
96 RequiredValueSyntax {
97 name: Cow::Borrowed("r"),
98 vtype: ExprType::Integer,
99 },
100 ArgSepSyntax::End,
101 ),
102 ],
103 None,
104 )])
105 .with_category(CATEGORY)
106 .with_description(
107 "Draws a circle of radius r centered at (x,y).
108The outline of the circle is drawn using the foreground color as selected by COLOR and the \
109area of the circle is left untouched.",
110 )
111 .build(),
112 console,
113 })
114 }
115}
116
117#[async_trait(?Send)]
118impl Callable for GfxCircleCommand {
119 fn metadata(&self) -> &CallableMetadata {
120 &self.metadata
121 }
122
123 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
124 debug_assert_eq!(3, scope.nargs());
125 let (xvalue, xpos) = scope.pop_integer_with_pos();
126 let (yvalue, ypos) = scope.pop_integer_with_pos();
127 let (rvalue, rpos) = scope.pop_integer_with_pos();
128
129 let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?;
130 let r = parse_radius(rvalue, rpos)?;
131
132 self.console.borrow_mut().draw_circle(xy, r)?;
133 Ok(())
134 }
135}
136
137pub struct GfxCirclefCommand {
139 metadata: CallableMetadata,
140 console: Rc<RefCell<dyn Console>>,
141}
142
143impl GfxCirclefCommand {
144 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
146 Rc::from(Self {
147 metadata: CallableMetadataBuilder::new("GFX_CIRCLEF")
148 .with_syntax(&[(
149 &[
150 SingularArgSyntax::RequiredValue(
151 RequiredValueSyntax {
152 name: Cow::Borrowed("x"),
153 vtype: ExprType::Integer,
154 },
155 ArgSepSyntax::Exactly(ArgSep::Long),
156 ),
157 SingularArgSyntax::RequiredValue(
158 RequiredValueSyntax {
159 name: Cow::Borrowed("y"),
160 vtype: ExprType::Integer,
161 },
162 ArgSepSyntax::Exactly(ArgSep::Long),
163 ),
164 SingularArgSyntax::RequiredValue(
165 RequiredValueSyntax {
166 name: Cow::Borrowed("r"),
167 vtype: ExprType::Integer,
168 },
169 ArgSepSyntax::End,
170 ),
171 ],
172 None,
173 )])
174 .with_category(CATEGORY)
175 .with_description(
176 "Draws a filled circle of radius r centered at (x,y).
177The outline and area of the circle are drawn using the foreground color as selected by COLOR.",
178 )
179 .build(),
180 console,
181 })
182 }
183}
184
185#[async_trait(?Send)]
186impl Callable for GfxCirclefCommand {
187 fn metadata(&self) -> &CallableMetadata {
188 &self.metadata
189 }
190
191 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
192 debug_assert_eq!(3, scope.nargs());
193 let (xvalue, xpos) = scope.pop_integer_with_pos();
194 let (yvalue, ypos) = scope.pop_integer_with_pos();
195 let (rvalue, rpos) = scope.pop_integer_with_pos();
196
197 let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?;
198 let r = parse_radius(rvalue, rpos)?;
199
200 self.console.borrow_mut().draw_circle_filled(xy, r)?;
201 Ok(())
202 }
203}
204
205pub struct GfxHeightFunction {
207 metadata: CallableMetadata,
208 console: Rc<RefCell<dyn Console>>,
209}
210
211impl GfxHeightFunction {
212 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
214 Rc::from(Self {
215 metadata: CallableMetadataBuilder::new("GFX_HEIGHT")
216 .with_return_type(ExprType::Integer)
217 .with_syntax(&[(&[], None)])
218 .with_category(CATEGORY)
219 .with_description(
220 "Returns the height in pixels of the graphical console.
221See GFX_WIDTH to query the other dimension.",
222 )
223 .build(),
224 console,
225 })
226 }
227}
228
229#[async_trait(?Send)]
230impl Callable for GfxHeightFunction {
231 fn metadata(&self) -> &CallableMetadata {
232 &self.metadata
233 }
234
235 async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
236 debug_assert_eq!(0, scope.nargs());
237 let size = self.console.borrow().size_pixels()?;
238 scope.return_integer(i32::from(size.height))
239 }
240}
241
242pub struct GfxLineCommand {
244 metadata: CallableMetadata,
245 console: Rc<RefCell<dyn Console>>,
246}
247
248impl GfxLineCommand {
249 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
251 Rc::from(Self {
252 metadata: CallableMetadataBuilder::new("GFX_LINE")
253 .with_syntax(&[(
254 &[
255 SingularArgSyntax::RequiredValue(
256 RequiredValueSyntax {
257 name: Cow::Borrowed("x1"),
258 vtype: ExprType::Integer,
259 },
260 ArgSepSyntax::Exactly(ArgSep::Long),
261 ),
262 SingularArgSyntax::RequiredValue(
263 RequiredValueSyntax {
264 name: Cow::Borrowed("y1"),
265 vtype: ExprType::Integer,
266 },
267 ArgSepSyntax::Exactly(ArgSep::Long),
268 ),
269 SingularArgSyntax::RequiredValue(
270 RequiredValueSyntax {
271 name: Cow::Borrowed("x2"),
272 vtype: ExprType::Integer,
273 },
274 ArgSepSyntax::Exactly(ArgSep::Long),
275 ),
276 SingularArgSyntax::RequiredValue(
277 RequiredValueSyntax {
278 name: Cow::Borrowed("y2"),
279 vtype: ExprType::Integer,
280 },
281 ArgSepSyntax::End,
282 ),
283 ],
284 None,
285 )])
286 .with_category(CATEGORY)
287 .with_description(
288 "Draws a line from (x1,y1) to (x2,y2).
289The line is drawn using the foreground color as selected by COLOR.",
290 )
291 .build(),
292 console,
293 })
294 }
295}
296
297#[async_trait(?Send)]
298impl Callable for GfxLineCommand {
299 fn metadata(&self) -> &CallableMetadata {
300 &self.metadata
301 }
302
303 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
304 debug_assert_eq!(4, scope.nargs());
305 let (x1value, x1pos) = scope.pop_integer_with_pos();
306 let (y1value, y1pos) = scope.pop_integer_with_pos();
307 let (x2value, x2pos) = scope.pop_integer_with_pos();
308 let (y2value, y2pos) = scope.pop_integer_with_pos();
309
310 let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?;
311 let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?;
312
313 self.console.borrow_mut().draw_line(x1y1, x2y2)?;
314 Ok(())
315 }
316}
317
318pub struct GfxPixelCommand {
320 metadata: CallableMetadata,
321 console: Rc<RefCell<dyn Console>>,
322}
323
324impl GfxPixelCommand {
325 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
327 Rc::from(Self {
328 metadata: CallableMetadataBuilder::new("GFX_PIXEL")
329 .with_syntax(&[(
330 &[
331 SingularArgSyntax::RequiredValue(
332 RequiredValueSyntax {
333 name: Cow::Borrowed("x"),
334 vtype: ExprType::Integer,
335 },
336 ArgSepSyntax::Exactly(ArgSep::Long),
337 ),
338 SingularArgSyntax::RequiredValue(
339 RequiredValueSyntax {
340 name: Cow::Borrowed("y"),
341 vtype: ExprType::Integer,
342 },
343 ArgSepSyntax::End,
344 ),
345 ],
346 None,
347 )])
348 .with_category(CATEGORY)
349 .with_description(
350 "Draws a pixel at (x,y).
351The pixel is drawn using the foreground color as selected by COLOR.",
352 )
353 .build(),
354 console,
355 })
356 }
357}
358
359#[async_trait(?Send)]
360impl Callable for GfxPixelCommand {
361 fn metadata(&self) -> &CallableMetadata {
362 &self.metadata
363 }
364
365 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
366 debug_assert_eq!(2, scope.nargs());
367 let (xvalue, xpos) = scope.pop_integer_with_pos();
368 let (yvalue, ypos) = scope.pop_integer_with_pos();
369
370 let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?;
371
372 self.console.borrow_mut().draw_pixel(xy)?;
373 Ok(())
374 }
375}
376
377pub struct GfxRectCommand {
379 metadata: CallableMetadata,
380 console: Rc<RefCell<dyn Console>>,
381}
382
383impl GfxRectCommand {
384 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
386 Rc::from(Self {
387 metadata: CallableMetadataBuilder::new("GFX_RECT")
388 .with_syntax(&[(
389 &[
390 SingularArgSyntax::RequiredValue(
391 RequiredValueSyntax {
392 name: Cow::Borrowed("x1"),
393 vtype: ExprType::Integer,
394 },
395 ArgSepSyntax::Exactly(ArgSep::Long),
396 ),
397 SingularArgSyntax::RequiredValue(
398 RequiredValueSyntax {
399 name: Cow::Borrowed("y1"),
400 vtype: ExprType::Integer,
401 },
402 ArgSepSyntax::Exactly(ArgSep::Long),
403 ),
404 SingularArgSyntax::RequiredValue(
405 RequiredValueSyntax {
406 name: Cow::Borrowed("x2"),
407 vtype: ExprType::Integer,
408 },
409 ArgSepSyntax::Exactly(ArgSep::Long),
410 ),
411 SingularArgSyntax::RequiredValue(
412 RequiredValueSyntax {
413 name: Cow::Borrowed("y2"),
414 vtype: ExprType::Integer,
415 },
416 ArgSepSyntax::End,
417 ),
418 ],
419 None,
420 )])
421 .with_category(CATEGORY)
422 .with_description(
423 "Draws a rectangle from (x1,y1) to (x2,y2).
424The outline of the rectangle is drawn using the foreground color as selected by COLOR and the \
425area of the rectangle is left untouched.",
426 )
427 .build(),
428 console,
429 })
430 }
431}
432
433#[async_trait(?Send)]
434impl Callable for GfxRectCommand {
435 fn metadata(&self) -> &CallableMetadata {
436 &self.metadata
437 }
438
439 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
440 debug_assert_eq!(4, scope.nargs());
441 let (x1value, x1pos) = scope.pop_integer_with_pos();
442 let (y1value, y1pos) = scope.pop_integer_with_pos();
443 let (x2value, x2pos) = scope.pop_integer_with_pos();
444 let (y2value, y2pos) = scope.pop_integer_with_pos();
445
446 let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?;
447 let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?;
448
449 self.console.borrow_mut().draw_rect(x1y1, x2y2)?;
450 Ok(())
451 }
452}
453
454pub struct GfxRectfCommand {
456 metadata: CallableMetadata,
457 console: Rc<RefCell<dyn Console>>,
458}
459
460impl GfxRectfCommand {
461 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
463 Rc::from(Self {
464 metadata: CallableMetadataBuilder::new("GFX_RECTF")
465 .with_syntax(&[(
466 &[
467 SingularArgSyntax::RequiredValue(
468 RequiredValueSyntax {
469 name: Cow::Borrowed("x1"),
470 vtype: ExprType::Integer,
471 },
472 ArgSepSyntax::Exactly(ArgSep::Long),
473 ),
474 SingularArgSyntax::RequiredValue(
475 RequiredValueSyntax {
476 name: Cow::Borrowed("y1"),
477 vtype: ExprType::Integer,
478 },
479 ArgSepSyntax::Exactly(ArgSep::Long),
480 ),
481 SingularArgSyntax::RequiredValue(
482 RequiredValueSyntax {
483 name: Cow::Borrowed("x2"),
484 vtype: ExprType::Integer,
485 },
486 ArgSepSyntax::Exactly(ArgSep::Long),
487 ),
488 SingularArgSyntax::RequiredValue(
489 RequiredValueSyntax {
490 name: Cow::Borrowed("y2"),
491 vtype: ExprType::Integer,
492 },
493 ArgSepSyntax::End,
494 ),
495 ],
496 None,
497 )])
498 .with_category(CATEGORY)
499 .with_description(
500 "Draws a filled rectangle from (x1,y1) to (x2,y2).
501The outline and area of the rectangle are drawn using the foreground color as selected by COLOR.",
502 )
503 .build(),
504 console,
505 })
506 }
507}
508
509#[async_trait(?Send)]
510impl Callable for GfxRectfCommand {
511 fn metadata(&self) -> &CallableMetadata {
512 &self.metadata
513 }
514
515 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
516 debug_assert_eq!(4, scope.nargs());
517 let (x1value, x1pos) = scope.pop_integer_with_pos();
518 let (y1value, y1pos) = scope.pop_integer_with_pos();
519 let (x2value, x2pos) = scope.pop_integer_with_pos();
520 let (y2value, y2pos) = scope.pop_integer_with_pos();
521
522 let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?;
523 let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?;
524
525 self.console.borrow_mut().draw_rect_filled(x1y1, x2y2)?;
526 Ok(())
527 }
528}
529
530pub struct GfxSyncCommand {
532 metadata: CallableMetadata,
533 console: Rc<RefCell<dyn Console>>,
534}
535
536impl GfxSyncCommand {
537 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
539 Rc::from(Self {
540 metadata: CallableMetadataBuilder::new("GFX_SYNC")
541 .with_syntax(&[
542 (&[], None),
543 (
544 &[SingularArgSyntax::RequiredValue(
545 RequiredValueSyntax {
546 name: Cow::Borrowed("enabled"),
547 vtype: ExprType::Boolean,
548 },
549 ArgSepSyntax::End,
550 )],
551 None,
552 ),
553 ])
554 .with_category(CATEGORY)
555 .with_description(
556 "Controls the video syncing flag and/or forces a sync.
557With no arguments, this command triggers a video sync without updating the video syncing flag. \
558When enabled? is specified, this updates the video syncing flag accordingly and triggers a video \
559sync if enabled? is TRUE.
560When video syncing is enabled, all console commands immediately refresh the console. This is \
561useful to see the effects of the commands right away, which is why this is the default mode in the \
562interpreter. However, this is a *very* inefficient way of drawing.
563When video syncing is disabled, all console updates are buffered until video syncing is enabled \
564again. This is perfect to draw complex graphics efficiently. If this is what you want to do, \
565you should disable syncing first, render a frame, call GFX_SYNC to flush the frame, repeat until \
566you are done, and then enable video syncing again. Note that the textual cursor is not visible \
567when video syncing is disabled.
568WARNING: Be aware that if you disable video syncing in the interactive interpreter, you will not \
569be able to see what you are typing any longer until you reenable video syncing.",
570 )
571 .build(),
572 console,
573 })
574 }
575}
576
577#[async_trait(?Send)]
578impl Callable for GfxSyncCommand {
579 fn metadata(&self) -> &CallableMetadata {
580 &self.metadata
581 }
582
583 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
584 if scope.nargs() == 0 {
585 self.console.borrow_mut().sync_now()?;
586 Ok(())
587 } else {
588 debug_assert_eq!(1, scope.nargs());
589 let enabled = scope.pop_boolean();
590
591 let mut console = self.console.borrow_mut();
592 if enabled {
593 console.show_cursor()?;
594 } else {
595 console.hide_cursor()?;
596 }
597 console.set_sync(enabled)?;
598 Ok(())
599 }
600 }
601}
602
603pub struct GfxWidthFunction {
605 metadata: CallableMetadata,
606 console: Rc<RefCell<dyn Console>>,
607}
608
609impl GfxWidthFunction {
610 pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
612 Rc::from(Self {
613 metadata: CallableMetadataBuilder::new("GFX_WIDTH")
614 .with_return_type(ExprType::Integer)
615 .with_syntax(&[(&[], None)])
616 .with_category(CATEGORY)
617 .with_description(
618 "Returns the width in pixels of the graphical console.
619See GFX_HEIGHT to query the other dimension.",
620 )
621 .build(),
622 console,
623 })
624 }
625}
626
627#[async_trait(?Send)]
628impl Callable for GfxWidthFunction {
629 fn metadata(&self) -> &CallableMetadata {
630 &self.metadata
631 }
632
633 async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
634 debug_assert_eq!(0, scope.nargs());
635 let size = self.console.borrow().size_pixels()?;
636 scope.return_integer(i32::from(size.width))
637 }
638}
639
640pub fn add_all(machine: &mut Machine, console: Rc<RefCell<dyn Console>>) {
642 machine.add_callable(GfxCircleCommand::new(console.clone()));
643 machine.add_callable(GfxCirclefCommand::new(console.clone()));
644 machine.add_callable(GfxHeightFunction::new(console.clone()));
645 machine.add_callable(GfxLineCommand::new(console.clone()));
646 machine.add_callable(GfxPixelCommand::new(console.clone()));
647 machine.add_callable(GfxRectCommand::new(console.clone()));
648 machine.add_callable(GfxRectfCommand::new(console.clone()));
649 machine.add_callable(GfxSyncCommand::new(console.clone()));
650 machine.add_callable(GfxWidthFunction::new(console));
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::console::SizeInPixels;
657 use crate::testutils::*;
658
659 fn check_errors_two_xy(name: &'static str) {
661 for args in &["1, 2, , 4", "1, 2, 3", "1, 2, 3, 4, 5", "2; 3, 4"] {
662 check_stmt_compilation_err(
663 format!("1:1: In call to {}: expected x1%, y1%, x2%, y2%", name),
664 &format!("{} {}", name, args),
665 );
666 }
667
668 for args in &["-40000, 1, 1, 1", "1, -40000, 1, 1", "1, 1, -40000, 1", "1, 1, 1, -40000"] {
669 let pos = name.len() + 1 + args.find('-').unwrap() + 1;
670 check_stmt_err(
671 format!("1:1: In call to {}: 1:{}: Coordinate -40000 out of range", name, pos),
672 &format!("{} {}", name, args),
673 );
674 }
675
676 for args in &["40000, 1, 1, 1", "1, 40000, 1, 1", "1, 1, 40000, 1", "1, 1, 1, 40000"] {
677 let pos = name.len() + 1 + args.find('4').unwrap() + 1;
678 check_stmt_err(
679 format!("1:1: In call to {}: 1:{}: Coordinate 40000 out of range", name, pos),
680 &format!("{} {}", name, args),
681 );
682 }
683
684 for args in &["\"a\", 1, 1, 1", "1, \"a\", 1, 1", "1, 1, \"a\", 1", "1, 1, 1, \"a\""] {
685 let stmt = &format!("{} {}", name, args);
686 let pos = stmt.find('"').unwrap() + 1;
687 check_stmt_compilation_err(
688 format!("1:1: In call to {}: 1:{}: STRING is not a number", name, pos),
689 stmt,
690 );
691 }
692 }
693
694 fn check_errors_xy_radius(name: &'static str) {
696 for args in &["1, , 3", "1, 2", "1, 2, 3, 4", "2; 3, 4"] {
697 check_stmt_compilation_err(
698 format!("1:1: In call to {}: expected x%, y%, r%", name),
699 &format!("{} {}", name, args),
700 );
701 }
702
703 for args in &["-40000, 1, 1", "1, -40000, 1"] {
704 let pos = name.len() + 1 + args.find('-').unwrap() + 1;
705 check_stmt_err(
706 format!("1:1: In call to {}: 1:{}: Coordinate -40000 out of range", name, pos),
707 &format!("{} {}", name, args),
708 );
709 }
710 check_stmt_err(
711 format!(
712 "1:1: In call to {}: 1:{}: Radius -40000 must be positive",
713 name,
714 name.len() + 8
715 ),
716 &format!("{} 1, 1, -40000", name),
717 );
718
719 for args in &["40000, 1, 1", "1, 40000, 1"] {
720 let pos = name.len() + 1 + args.find('4').unwrap() + 1;
721 check_stmt_err(
722 format!("1:1: In call to {}: 1:{}: Coordinate 40000 out of range", name, pos),
723 &format!("{} {}", name, args),
724 );
725 }
726 check_stmt_err(
727 format!("1:1: In call to {}: 1:{}: Radius 80000 out of range", name, name.len() + 8),
728 &format!("{} 1, 1, 80000", name),
729 );
730
731 for args in &["\"a\", 1, 1", "1, \"a\", 1", "1, 1, \"a\""] {
732 let stmt = &format!("{} {}", name, args);
733 let pos = stmt.find('"').unwrap() + 1;
734 check_stmt_compilation_err(
735 format!("1:1: In call to {}: 1:{}: STRING is not a number", name, pos),
736 stmt,
737 );
738 }
739
740 check_stmt_err(
741 format!("1:1: In call to {}: 1:{}: Radius -1 must be positive", name, name.len() + 8),
742 &format!("{} 1, 1, -1", name),
743 );
744 }
745
746 #[test]
747 fn test_gfx_circle_ok() {
748 Tester::default()
749 .run("GFX_CIRCLE 0, 0, 0")
750 .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 0, y: 0 }, 0)])
751 .check();
752
753 Tester::default()
754 .run("GFX_CIRCLE 1.1, 2.3, 2.5")
755 .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 1, y: 2 }, 3)])
756 .check();
757
758 Tester::default()
759 .run("GFX_CIRCLE -31000, -32000, 31000")
760 .expect_output([CapturedOut::DrawCircle(PixelsXY { x: -31000, y: -32000 }, 31000)])
761 .check();
762 }
763
764 #[test]
765 fn test_gfx_circle_errors() {
766 check_errors_xy_radius("GFX_CIRCLE");
767 }
768
769 #[test]
770 fn test_gfx_circlef_ok() {
771 Tester::default()
772 .run("GFX_CIRCLEF 0, 0, 0")
773 .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 0, y: 0 }, 0)])
774 .check();
775
776 Tester::default()
777 .run("GFX_CIRCLEF 1.1, 2.3, 2.5")
778 .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 1, y: 2 }, 3)])
779 .check();
780
781 Tester::default()
782 .run("GFX_CIRCLEF -31000, -32000, 31000")
783 .expect_output([CapturedOut::DrawCircleFilled(
784 PixelsXY { x: -31000, y: -32000 },
785 31000,
786 )])
787 .check();
788 }
789
790 #[test]
791 fn test_gfx_circlef_errors() {
792 check_errors_xy_radius("GFX_CIRCLEF");
793 }
794
795 #[test]
796 fn test_gfx_height() {
797 let mut t = Tester::default();
798 t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(1, 768));
799 t.run("result = GFX_HEIGHT").expect_var("result", 768i32).check();
800
801 check_expr_error(
802 "1:10: In call to GFX_HEIGHT: Graphical console size not yet set",
803 "GFX_HEIGHT",
804 );
805
806 check_expr_compilation_error(
807 "1:10: In call to GFX_HEIGHT: expected no arguments nor parenthesis",
808 "GFX_HEIGHT()",
809 );
810 check_expr_compilation_error(
811 "1:10: In call to GFX_HEIGHT: expected no arguments nor parenthesis",
812 "GFX_HEIGHT(1)",
813 );
814 }
815
816 #[test]
817 fn test_gfx_line_ok() {
818 Tester::default()
819 .run("GFX_LINE 1, 2, 3, 4")
820 .expect_output([CapturedOut::DrawLine(
821 PixelsXY { x: 1, y: 2 },
822 PixelsXY { x: 3, y: 4 },
823 )])
824 .check();
825
826 Tester::default()
827 .run("GFX_LINE -31000.3, -32000.2, 31000.4, 31999.8")
828 .expect_output([CapturedOut::DrawLine(
829 PixelsXY { x: -31000, y: -32000 },
830 PixelsXY { x: 31000, y: 32000 },
831 )])
832 .check();
833 }
834
835 #[test]
836 fn test_gfx_line_errors() {
837 check_errors_two_xy("GFX_LINE");
838 }
839
840 #[test]
841 fn test_gfx_pixel_ok() {
842 Tester::default()
843 .run("GFX_PIXEL 1, 2")
844 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
845 .check();
846
847 Tester::default()
848 .run("GFX_PIXEL -31000, -32000")
849 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: -31000, y: -32000 })])
850 .check();
851
852 Tester::default()
853 .run("GFX_PIXEL 30999.5, 31999.7")
854 .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 31000, y: 32000 })])
855 .check();
856 }
857
858 #[test]
859 fn test_gfx_pixel_errors() {
860 for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1", "GFX_PIXEL 1; 2"] {
861 check_stmt_compilation_err("1:1: In call to GFX_PIXEL: expected x%, y%", cmd);
862 }
863
864 for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] {
865 check_stmt_err(
866 format!(
867 "1:1: In call to GFX_PIXEL: 1:{}: Coordinate -40000 out of range",
868 cmd.find('-').unwrap() + 1
869 ),
870 cmd,
871 );
872 }
873
874 for cmd in &["GFX_PIXEL \"a\", 1", "GFX_PIXEL 1, \"a\""] {
875 let pos = cmd.find('"').unwrap() + 1;
876 check_stmt_compilation_err(
877 format!("1:1: In call to GFX_PIXEL: 1:{}: STRING is not a number", pos),
878 cmd,
879 );
880 }
881 }
882
883 #[test]
884 fn test_gfx_rect_ok() {
885 Tester::default()
886 .run("GFX_RECT 1.1, 2.3, 2.5, 3.9")
887 .expect_output([CapturedOut::DrawRect(
888 PixelsXY { x: 1, y: 2 },
889 PixelsXY { x: 3, y: 4 },
890 )])
891 .check();
892
893 Tester::default()
894 .run("GFX_RECT -31000, -32000, 31000, 32000")
895 .expect_output([CapturedOut::DrawRect(
896 PixelsXY { x: -31000, y: -32000 },
897 PixelsXY { x: 31000, y: 32000 },
898 )])
899 .check();
900 }
901
902 #[test]
903 fn test_gfx_rect_errors() {
904 check_errors_two_xy("GFX_RECT");
905 }
906
907 #[test]
908 fn test_gfx_rectf_ok() {
909 Tester::default()
910 .run("GFX_RECTF 1.1, 2.3, 2.5, 3.9")
911 .expect_output([CapturedOut::DrawRectFilled(
912 PixelsXY { x: 1, y: 2 },
913 PixelsXY { x: 3, y: 4 },
914 )])
915 .check();
916
917 Tester::default()
918 .run("GFX_RECTF -31000, -32000, 31000, 32000")
919 .expect_output([CapturedOut::DrawRectFilled(
920 PixelsXY { x: -31000, y: -32000 },
921 PixelsXY { x: 31000, y: 32000 },
922 )])
923 .check();
924 }
925
926 #[test]
927 fn test_gfx_rectf_errors() {
928 check_errors_two_xy("GFX_RECTF");
929 }
930
931 #[test]
932 fn test_gfx_sync_ok() {
933 Tester::default().run("GFX_SYNC").expect_output([CapturedOut::SyncNow]).check();
934 Tester::default()
935 .run("GFX_SYNC TRUE")
936 .expect_output([CapturedOut::ShowCursor, CapturedOut::SetSync(true)])
937 .check();
938 Tester::default()
939 .run("GFX_SYNC FALSE")
940 .expect_output([CapturedOut::HideCursor, CapturedOut::SetSync(false)])
941 .check();
942 }
943
944 #[test]
945 fn test_gfx_sync_errors() {
946 check_stmt_compilation_err(
947 "1:1: In call to GFX_SYNC: expected <> | <enabled?>",
948 "GFX_SYNC 2, 3",
949 );
950 check_stmt_compilation_err(
951 "1:1: In call to GFX_SYNC: 1:10: INTEGER is not a BOOLEAN",
952 "GFX_SYNC 2",
953 );
954 }
955
956 #[test]
957 fn test_gfx_width() {
958 let mut t = Tester::default();
959 t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(12345, 1));
960 t.run("result = GFX_WIDTH").expect_var("result", 12345i32).check();
961
962 check_expr_error(
963 "1:10: In call to GFX_WIDTH: Graphical console size not yet set",
964 "GFX_WIDTH",
965 );
966
967 check_expr_compilation_error(
968 "1:10: In call to GFX_WIDTH: expected no arguments nor parenthesis",
969 "GFX_WIDTH()",
970 );
971 check_expr_compilation_error(
972 "1:10: In call to GFX_WIDTH: expected no arguments nor parenthesis",
973 "GFX_WIDTH(1)",
974 );
975 }
976}