Skip to main content

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}