plotters_wxdragon/lib.rs
1//! `plotters-wxdragon` is a backend for [Plotters], to draw plots in a GUI window
2//! managed by [wxDragon].
3//!
4//! [wxDragon] is a binding of wxWidgets for rust. wxWidgets is a cross-platform
5//! GUI library toolkit for desktop applications, that uses the native GUI toolkit
6//! on Windows, macOS, and Linux.
7//!
8//! [Plotters] is a Rust drawing library focusing on data plotting for both WASM
9//! and native applications.
10//!
11//! [Plotters]: https://crates.io/crates/plotters
12//! [wxDragon]: https://crates.io/crates/wxdragon
13//!
14//! ## Getting started
15//!
16//! ### Quick start
17//!
18//! 1. Follow [wxDragon] instructions to install the `wxdragon` crate. You will
19//! need the wxWidgets library for your OS so be sure to follow the
20//! instructions.
21//!
22//! 2. Clone this repository and run the `x2` plotting example to check that it
23//! works for you.
24//!
25//! ```bash
26//! git clone https://github.com/threefold3/plotters-wxdragon.git
27//! cd plotters-wxdragon
28//! cargo run --example x2
29//! ```
30//!
31//! This will open a new window displaying a simple `y=x^2` plot.
32//!
33//! ### Integrating with a project
34//!
35//! 1. Add the following to your `Cargo.toml` file:
36//!
37//! ```toml
38//! [dependencies]
39//! wxdragon = "0.9"
40//! plotters = "0.3"
41//! plotters-wxdragon = "0.1"
42//! ```
43//!
44//! 2. Create an app with a `wxdragon::Panel`, and use the panel's `on_paint`
45//! handler to create a new `wxdragon::AutoBufferedPaintDC` (device context)
46//! each time, wrap it in a `WxBackend` and then draw on it.
47//!
48//! ```rust
49//! use plotters::prelude::*;
50//! use plotters::style::text_anchor::{HPos, Pos, VPos};
51//! use plotters_wxdragon::WxBackend;
52//! use wxdragon::{self as wx, WindowEvents, WxWidget};
53//!
54//! struct DrawingPanel {
55//! panel: wx::Panel,
56//! }
57//!
58//! impl DrawingPanel {
59//! fn new(parent: &wx::Frame) -> Self {
60//! let panel = wx::PanelBuilder::new(parent).build();
61//! panel.set_background_style(wx::BackgroundStyle::Paint);
62//!
63//! // Register the paint handler with a move closure
64//! panel.on_paint(move |_event| {
65//! // Create a device context (wxdragon has several types)
66//! // and a plotters backend
67//! let dc = wx::AutoBufferedPaintDC::new(&panel);
68//! let mut backend = WxBackend::new(&dc);
69//!
70//! // Create a plotters plot as you would with any other backend
71//! backend
72//! .draw_rect((300, 250), (500, 350), &BLACK, false)
73//! .unwrap();
74//! let style = TextStyle::from(("monospace", 32.0).into_font())
75//! .pos(Pos::new(HPos::Center, VPos::Center));
76//! backend
77//! .draw_text("hello, world", &style, (400, 300))
78//! .unwrap();
79//!
80//! // Call present() when you are ready
81//! backend.present().expect("present");
82//! });
83//!
84//! // Handle SIZE events to refresh when the window size changes
85//! panel.on_size(move |_event| {
86//! panel.refresh(true, None);
87//! });
88//!
89//! Self { panel }
90//! }
91//! }
92//!
93//! impl std::ops::Deref for DrawingPanel {
94//! type Target = wx::Panel;
95//!
96//! fn deref(&self) -> &Self::Target {
97//! &self.panel
98//! }
99//! }
100//!
101//! fn main() {
102//! let _ = wxdragon::main(|_| {
103//! let frame = wx::Frame::builder()
104//! .with_title("Getting started")
105//! // with this, wx produces a canvas of size 800 x 600
106//! .with_size(wx::Size::new(852, 689))
107//! .build();
108//!
109//! let drawing_panel = DrawingPanel::new(&frame);
110//!
111//! // Initial paint
112//! drawing_panel.refresh(true, None);
113//!
114//! frame.show(true);
115//! });
116//! }
117//! ```
118//!
119//! You can find more details in the examples for how to integrate a plot in
120//! your wxWidgets application:
121//! + `x2`: simple `y=x^2` plot in a wxWidgets frame
122//! + `text`: a single window that shows various text orientations and a
123//! toolbar that can modify application state
124//!
125//! See also the existing tests, which illustrate that most existing
126//! plotters examples work without change. In these tests we write to to an
127//! in-memory device context instead of a device context linked to a
128//! `Panel`, and compare to a reference png images to ensure non-regression.
129//!
130//! ## How this works
131//!
132//! This crate implements a backend for [Plotters]. It uses the existing drawing
133//! context of wxWidgets, and maps plotters drawing primitives to corresponding
134//! calls fo the wxWidgets API.
135//!
136//! See also [`plotters-backend`] for reference on implementing a backend for
137//! plotters.
138//!
139//! [`plotters-backend`]: https://docs.rs/plotters-backend/latest/plotters_backend/
140//!
141//! ## License
142//!
143//! This project is dual-licensed under [Apache 2.0](./LICENSE-APACHE) and
144//! [`MIT`](./LICENSE-MIT) terms.
145
146use plotters_backend::{
147 BackendColor, DrawingBackend, FontFamily, FontStyle, FontTransform,
148 text_anchor::{HPos, Pos, VPos},
149};
150use wxdragon::{self as wx, BackgroundMode, DeviceContext};
151
152/// Bridge struct to allow plotters to plot on a [`wxdragon::DeviceContext`].
153///
154/// This backend works with any [`wxdragon::DeviceContext`] that implements the
155/// required drawing primitives. For example:
156/// - [`wxdragon::AutoBufferedPaintDC`] for drawing on a [`wxdragon::Panel`]
157/// in a GUI application.
158/// - [`wxdragon::MemoryDC`] for off-screen drawing to a [`wxdragon::Bitmap`].
159///
160/// # How to use
161///
162/// Create an app with a `wxdragon::Panel`, and use the panel's `on_paint`
163/// handler to create a new `wxdragon::AutoBufferedPaintDC` (device context)
164/// each time, wrap it in a `WxBackend` and then draw on it.
165///
166/// ```no_run
167/// use plotters::prelude::*;
168/// use plotters::style::text_anchor::{HPos, Pos, VPos};
169/// use plotters_wxdragon::WxBackend;
170/// use wxdragon::{self as wx, WindowEvents, WxWidget};
171///
172/// struct DrawingPanel {
173/// panel: wx::Panel,
174/// }
175///
176/// impl DrawingPanel {
177/// fn new(parent: &wx::Frame) -> Self {
178/// let panel = wx::PanelBuilder::new(parent).build();
179/// panel.set_background_style(wx::BackgroundStyle::Paint);
180///
181/// // Register the paint handler with a move closure
182/// panel.on_paint(move |_event| {
183/// // Create a device context (wxdragon has several types)
184/// // and a plotters backend
185/// let dc = wx::AutoBufferedPaintDC::new(&panel);
186/// let mut backend = WxBackend::new(&dc);
187///
188/// // Create a plotters plot as you would with any other backend
189/// backend
190/// .draw_rect((300, 250), (500, 350), &BLACK, false)
191/// .unwrap();
192/// let style = TextStyle::from(("monospace", 32.0).into_font())
193/// .pos(Pos::new(HPos::Center, VPos::Center));
194/// backend
195/// .draw_text("hello, world", &style, (400, 300))
196/// .unwrap();
197///
198/// // Call present() when you are ready
199/// backend.present().expect("present");
200/// });
201///
202/// // Handle SIZE events to refresh when the window size changes
203/// panel.on_size(move |_event| {
204/// panel.refresh(true, None);
205/// });
206///
207/// Self { panel }
208/// }
209/// }
210///
211/// impl std::ops::Deref for DrawingPanel {
212/// type Target = wx::Panel;
213///
214/// fn deref(&self) -> &Self::Target {
215/// &self.panel
216/// }
217/// }
218///
219/// let _ = wxdragon::main(|_| {
220/// let frame = wx::Frame::builder()
221/// .with_title("Getting started")
222/// // with this, wx produces a canvas of size 800 x 600
223/// .with_size(wx::Size::new(852, 689))
224/// .build();
225///
226/// let drawing_panel = DrawingPanel::new(&frame);
227///
228/// // Initial paint
229/// drawing_panel.refresh(true, None);
230///
231/// frame.show(true);
232/// });
233/// ```
234pub struct WxBackend<'context, C>
235where
236 C: DeviceContext,
237{
238 context: &'context C,
239}
240
241impl<'context, C> WxBackend<'context, C>
242where
243 C: DeviceContext,
244{
245 /// Creates a new `WxBackend` from a `wxdragon::DeviceContext`.
246 ///
247 /// The `DeviceContext` is initialized with a white background color and
248 /// transparent background mode.
249 pub fn new(context: &'context C) -> WxBackend<'context, C> {
250 let backend = WxBackend { context };
251 backend.set_background_color(wx::Colour::rgb(255, 255, 255));
252 backend.set_background_mode(wx::BackgroundMode::Transparent);
253 backend.clear();
254 backend
255 }
256
257 /// Clear the device context.
258 pub fn clear(&self) {
259 self.context.clear();
260 }
261
262 /// Set the background color of the device context.
263 ///
264 /// This setting affects the global background, and also the fill color of
265 /// text labels.
266 pub fn set_background_color(&self, color: wx::Colour) {
267 self.context.set_background(color);
268 }
269
270 /// Set the background mode of the device context.
271 ///
272 /// This settings affects the fill color of text labels. Use
273 /// [`BackgroundMode::Solid`] if you want text labels to have a solid
274 /// background, otherwise leave the default
275 /// [`BackgroundMode::Transparent`].
276 pub fn set_background_mode(&self, mode: BackgroundMode) {
277 self.context.set_background_mode(mode);
278 }
279
280 /// Set pen from plotters style.
281 fn set_pen_style<S: plotters_backend::BackendStyle>(&self, style: &S) {
282 let color = convert_color(style.color());
283 let width = style.stroke_width() as i32;
284 // FIXME: how to get info of other styles?
285 let style = wx::PenStyle::Solid;
286 self.context.set_pen(color, width, style);
287 }
288
289 /// Set brush from plotters style.
290 fn set_brush_style(
291 &self,
292 fill: bool,
293 color: plotters_backend::BackendColor,
294 ) {
295 let style = match fill {
296 true => wx::BrushStyle::Solid,
297 false => wx::BrushStyle::Transparent,
298 };
299 let color = convert_color(color);
300 self.context.set_brush(color, style);
301 }
302
303 /// Sets the font style from plotters BackendTextStyle.
304 ///
305 /// Note: text background information is not present in
306 /// plotters_backend::BackendTextStyle, but it can be controlled using
307 /// [`WxBackend::set_background_mode`] and
308 /// [`WxBackend::set_background_color`]
309 fn set_font_style<TStyle: plotters_backend::BackendTextStyle>(
310 &self,
311 style: &TStyle,
312 ) -> Result<(), ErrorInner> {
313 self.context
314 .set_text_background(self.context.get_background());
315 let color = convert_color(style.color());
316 self.context.set_text_foreground(color);
317 // FIXME: There is a discrepancy with font size compared to the
318 // BitmapBackend. For now using a coeficient 0.6. Note that in the
319 // tests of an off-screen wxBitmap, the dpi value is 96.
320 let point_size = (style.size() * 0.6) as i32;
321 let (family, face_name) = match style.family() {
322 // According to wx docs
323 // https://docs.wxwidgets.org/3.2/interface_2wx_2font_8h.html
324 FontFamily::Monospace => (wx::FontFamily::Teletype, "None"),
325 FontFamily::SansSerif => (wx::FontFamily::Swiss, "None"),
326 FontFamily::Serif => (wx::FontFamily::Roman, "None"),
327 FontFamily::Name(name) => (wx::FontFamily::Default, name),
328 };
329 use wx::FontStyle::{Italic, Normal, Slant};
330 let (style, weight) = match style.style() {
331 FontStyle::Bold => (Normal, wx::FontWeight::Bold),
332 FontStyle::Italic => (Italic, wx::FontWeight::Normal),
333 FontStyle::Normal => (Normal, wx::FontWeight::Normal),
334 FontStyle::Oblique => (Slant, wx::FontWeight::Normal),
335 };
336 let underlined = false;
337 let font = wx::Font::builder()
338 .with_point_size(point_size)
339 .with_family(family)
340 .with_style(style)
341 .with_weight(weight)
342 .with_underline(underlined)
343 // NOTE: wxdragon could be improved here. `with_face_name()`
344 // creates a string, and `build()` creates another string in its
345 // call to `wx::dc::Font::new_with_details()`.
346 .with_face_name(face_name)
347 .build()
348 .ok_or(ErrorInner::CreateFont)?;
349 self.context.set_font(&font);
350 Ok(())
351 }
352}
353
354/// Convert color from plotters to wx
355fn convert_color(color: plotters_backend::BackendColor) -> wx::Colour {
356 let BackendColor { alpha, rgb } = color;
357 let (r, g, b) = rgb;
358 wx::Colour::new(r, g, b, (alpha * 255.0) as u8)
359}
360
361impl<'context, C> DrawingBackend for WxBackend<'context, C>
362where
363 C: DeviceContext,
364{
365 type ErrorType = Error;
366 fn get_size(&self) -> (u32, u32) {
367 let (width, height) = self.context.get_size();
368 (width as u32, height as u32)
369 }
370
371 fn ensure_prepared(
372 &mut self,
373 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
374 Ok(())
375 }
376
377 fn present(
378 &mut self,
379 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
380 Ok(())
381 }
382
383 fn draw_pixel(
384 &mut self,
385 point: plotters_backend::BackendCoord,
386 color: plotters_backend::BackendColor,
387 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
388 let (x, y) = point;
389 let color = convert_color(color);
390 let width = 1;
391 let style = wx::PenStyle::Solid;
392 self.context.set_pen(color, width, style);
393 self.context.draw_point(x, y);
394 Ok(())
395 }
396
397 fn draw_line<S: plotters_backend::BackendStyle>(
398 &mut self,
399 from: plotters_backend::BackendCoord,
400 to: plotters_backend::BackendCoord,
401 style: &S,
402 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
403 self.set_pen_style(style);
404 let (x1, y1) = from;
405 let (x2, y2) = to;
406 self.context.draw_line(x1, y1, x2, y2);
407 Ok(())
408 }
409
410 fn draw_path<
411 S: plotters_backend::BackendStyle,
412 I: IntoIterator<Item = plotters_backend::BackendCoord>,
413 >(
414 &mut self,
415 path: I,
416 style: &S,
417 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
418 self.set_pen_style(style);
419 let points: Vec<wx::dc::Point> = path
420 .into_iter()
421 .map(|(x, y)| wx::dc::Point::new(x, y))
422 .collect();
423 let x_offset = 0;
424 let y_offset = 0;
425 self.context.draw_lines(&points[..], x_offset, y_offset);
426 Ok(())
427 }
428
429 fn draw_circle<S: plotters_backend::BackendStyle>(
430 &mut self,
431 center: plotters_backend::BackendCoord,
432 radius: u32,
433 style: &S,
434 fill: bool,
435 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
436 self.set_pen_style(style);
437 self.set_brush_style(fill, style.color());
438 let (x, y) = center;
439 self.context.draw_circle(x, y, radius as i32);
440 Ok(())
441 }
442
443 fn draw_rect<S: plotters_backend::BackendStyle>(
444 &mut self,
445 upper_left: plotters_backend::BackendCoord,
446 bottom_right: plotters_backend::BackendCoord,
447 style: &S,
448 fill: bool,
449 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
450 self.set_pen_style(style);
451 self.set_brush_style(fill, style.color());
452 let (x1, y1) = upper_left;
453 let (x2, y2) = bottom_right;
454 let width = x2 - x1;
455 let height = y2 - y1;
456 self.context.draw_rectangle(x1, y1, width, height);
457 Ok(())
458 }
459
460 fn fill_polygon<
461 S: plotters_backend::BackendStyle,
462 I: IntoIterator<Item = plotters_backend::BackendCoord>,
463 >(
464 &mut self,
465 vert: I,
466 style: &S,
467 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
468 self.set_pen_style(style);
469 self.set_brush_style(true, style.color());
470 let points: Vec<wx::dc::Point> = vert
471 .into_iter()
472 .map(|(x, y)| wx::dc::Point::new(x, y))
473 .collect();
474 let x_offset = 0;
475 let y_offset = 0;
476 let fill_mode = wx::dc::PolygonFillMode::OddEven;
477 self.context
478 .draw_polygon(&points[..], x_offset, y_offset, fill_mode);
479 Ok(())
480 }
481
482 fn draw_text<TStyle: plotters_backend::BackendTextStyle>(
483 &mut self,
484 text: &str,
485 style: &TStyle,
486 pos: plotters_backend::BackendCoord,
487 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
488 // this also sets the font style
489 let (width, height) = self.estimate_text_size(text, style)?;
490 let width = width as i32;
491 let height = height as i32;
492 let (x, y) = pos;
493
494 // plotters convention is that anchor position is relative to
495 // character's point of view
496 let Pos { h_pos, v_pos } = style.anchor();
497 let dx = match h_pos {
498 HPos::Left => 0,
499 HPos::Center => -width / 2,
500 HPos::Right => -width,
501 };
502 let dy = match v_pos {
503 VPos::Top => 0,
504 VPos::Center => -height / 2,
505 VPos::Bottom => -height,
506 };
507 let (dx, dy) = match style.transform() {
508 FontTransform::None => (dx, dy),
509 FontTransform::Rotate90 => (-dy, dx),
510 FontTransform::Rotate180 => (-dx, -dy),
511 FontTransform::Rotate270 => (dy, -dx),
512 };
513
514 // plotters rotates clockwise, wxwidgets rotates counterclockwise
515 let angle = match style.transform() {
516 FontTransform::None => None,
517 FontTransform::Rotate90 => Some(-90.0),
518 FontTransform::Rotate180 => Some(-180.0),
519 FontTransform::Rotate270 => Some(-270.0),
520 };
521 if let Some(angle) = angle {
522 self.context.draw_rotated_text(text, x + dx, y + dy, angle);
523 } else {
524 self.context.draw_text(text, x + dx, y + dy);
525 }
526 Ok(())
527 }
528
529 fn estimate_text_size<TStyle: plotters_backend::BackendTextStyle>(
530 &self,
531 text: &str,
532 style: &TStyle,
533 ) -> Result<(u32, u32), plotters_backend::DrawingErrorKind<Self::ErrorType>>
534 {
535 self.set_font_style(style).map_err(|e| {
536 plotters_backend::DrawingErrorKind::FontError(Box::new(Error(e)))
537 })?;
538 let (width, height) = self.context.get_text_extent(text);
539 Ok((width as u32, height as u32))
540 }
541
542 fn blit_bitmap(
543 &mut self,
544 pos: plotters_backend::BackendCoord,
545 (iw, ih): (u32, u32),
546 src: &[u8],
547 ) -> Result<(), plotters_backend::DrawingErrorKind<Self::ErrorType>> {
548 let (x, y) = pos;
549 let bitmap = wx::Bitmap::from_rgba(src, iw, ih).ok_or_else(|| {
550 plotters_backend::DrawingErrorKind::FontError(Box::new(Error(
551 ErrorInner::CreateBitmap,
552 )))
553 })?;
554 let transparent = false; // FIXME
555 self.context.draw_bitmap(&bitmap, x, y, transparent);
556 Ok(())
557 }
558}
559
560/// Represents an error when drawing on a [`WxBackend`].
561#[derive(Debug, thiserror::Error)]
562#[error(transparent)]
563pub struct Error(#[from] ErrorInner);
564
565/// Error kind for `plotters_wxdragon::Error`.
566#[derive(Debug, thiserror::Error)]
567enum ErrorInner {
568 #[error("failed to create font from plotters BackendTextStyle")]
569 CreateFont,
570 #[error("failed to create bitmap")]
571 CreateBitmap,
572}