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