diff --git a/Cargo.toml b/Cargo.toml
index 1ad747d81..99826fe31 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ members = [
"backends/conrod_rendy",
"backends/conrod_vulkano",
"backends/conrod_wgpu",
+ "backends/conrod_sdl2",
]
# wgpu requires this feature resolver version.
diff --git a/backends/conrod_sdl2/Cargo.toml b/backends/conrod_sdl2/Cargo.toml
new file mode 100644
index 000000000..0a68d03e0
--- /dev/null
+++ b/backends/conrod_sdl2/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "conrod_sdl2"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+arrayvec = "0.7.2"
+conrod_core = { path = "../../conrod_core", version = "0.76" }
+thiserror = "1.0.30"
+sdl2 = { version = "0.35.1", features = ["gfx", "image"] }
+
+[dev-dependencies]
+conrod_example_shared = { path = "../conrod_example_shared", version = "0.76" }
+find_folder = "0.3.0"
diff --git a/backends/conrod_sdl2/examples/all_sdl2.rs b/backends/conrod_sdl2/examples/all_sdl2.rs
new file mode 100644
index 000000000..0da5b8890
--- /dev/null
+++ b/backends/conrod_sdl2/examples/all_sdl2.rs
@@ -0,0 +1,94 @@
+use std::{thread::sleep, time::Duration};
+
+use conrod_core::UiBuilder;
+use conrod_example_shared::{DemoApp, WIN_H, WIN_W};
+use conrod_sdl2::{convert_event, DrawPrimitiveError};
+use sdl2::{
+ image::LoadTexture, render::TextureValueError, video::WindowBuildError, IntegerOrSdlError,
+};
+use thiserror::Error;
+
+fn main() -> Result<(), SdlError> {
+ let sdl = sdl2::init().map_err(SdlError::String)?;
+
+ let video_subsystem = sdl.video().map_err(SdlError::String)?;
+ let window = video_subsystem
+ .window("Conrod with SDL2!", WIN_W, WIN_H)
+ .resizable()
+ .allow_highdpi()
+ .build()?;
+ let mut canvas = window.into_canvas().present_vsync().build()?;
+ let texture_creator = canvas.texture_creator();
+
+ let mut event_pump = sdl.event_pump().map_err(SdlError::String)?;
+
+ // The assets directory, where the Rust logo and the font file belong.
+ let assets = find_folder::Search::KidsThenParents(3, 5)
+ .for_folder("assets")
+ .expect("Assets directory not found");
+
+ // Load Rust logo as a texture.
+ let rust_logo_path = assets.join("images/rust.png");
+ let mut image_map = conrod_core::image::Map::new();
+ let rust_logo = texture_creator
+ .load_texture(rust_logo_path)
+ .map_err(SdlError::String)?;
+ let rust_logo = image_map.insert(rust_logo);
+
+ // Create Ui and Ids of widgets to instantiate
+ let mut ui = UiBuilder::new([WIN_W as f64, WIN_H as f64])
+ .theme(conrod_example_shared::theme())
+ .build();
+ let ids = conrod_example_shared::Ids::new(ui.widget_id_generator());
+ // Demonstration app state that we'll control with our conrod GUI.
+ let mut app = DemoApp::new(rust_logo);
+
+ // Load font file
+ let font_path = assets.join("fonts/NotoSans/NotoSans-Regular.ttf");
+ ui.fonts.insert_from_file(font_path).unwrap();
+
+ // Renderer
+ let mut renderer = conrod_sdl2::Renderer::new(&texture_creator)?;
+
+ 'main: loop {
+ for event in event_pump.poll_iter() {
+ match event {
+ sdl2::event::Event::Quit { .. }
+ | sdl2::event::Event::KeyDown {
+ keycode: Some(sdl2::keyboard::Keycode::Escape),
+ ..
+ } => break 'main,
+ _ => {}
+ }
+ for event in convert_event(event, canvas.window().size()) {
+ ui.handle_event(event);
+ }
+ }
+
+ conrod_example_shared::gui(&mut ui.set_widgets(), &ids, &mut app);
+ if let Some(primitives) = ui.draw_if_changed() {
+ renderer.draw(&mut canvas, &mut image_map, primitives)?;
+ canvas.present();
+ } else {
+ // We should sleep for a reasonable duration before polloing for new events
+ // in order to avoid polloing new events very frequently.
+ sleep(Duration::from_secs_f64(1. / 60.));
+ }
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, Error)]
+pub enum SdlError {
+ #[error("SDL returned an error: {0}")]
+ String(String),
+ #[error("SDL failed to build a window: {0}")]
+ WindowBuildError(#[from] WindowBuildError),
+ #[error("Integer overflowed or SDL returned an error: {0}")]
+ IntegerOrSdlError(#[from] IntegerOrSdlError),
+ #[error("Failed to create a texture: {0}")]
+ TextureValueError(#[from] TextureValueError),
+ #[error("Failed to draw a primitive: {0}")]
+ DrawPrimitiveError(#[from] DrawPrimitiveError),
+}
diff --git a/backends/conrod_sdl2/src/lib.rs b/backends/conrod_sdl2/src/lib.rs
new file mode 100644
index 000000000..e3a7469ab
--- /dev/null
+++ b/backends/conrod_sdl2/src/lib.rs
@@ -0,0 +1,461 @@
+use arrayvec::ArrayVec;
+use conrod_core::{
+ event::Input,
+ input,
+ render::{Primitive, PrimitiveKind, PrimitiveWalker},
+ text::{
+ rt::gpu_cache::{CacheReadErr, CacheWriteErr},
+ GlyphCache,
+ },
+ widget::triangles::{ColoredPoint, Triangle},
+ Color as ConrodColor, Point as ConrodPoint, Scalar,
+};
+use sdl2::{
+ event::{Event, WindowEvent},
+ gfx::primitives::DrawRenderer,
+ pixels::PixelFormatEnum,
+ rect::Rect as SdlRect,
+ render::{BlendMode, Canvas, Texture, TextureCreator, TextureValueError},
+ video::{Window, WindowContext},
+};
+use thiserror::Error;
+
+type SdlWindowSize = (u32, u32);
+
+/// Converts a `sdl2::event::Event` to a `conrod::event::Input`.
+///
+/// This can be useful for single-window applications. Note that window_size should be the dpi
+/// agnostic values (i.e. pass `window.size()` values and NOT `window.drawable_size()`).
+///
+/// NOTE: The sdl2 MouseMotion event is a combination of a MouseCursor and MouseRelative conrod
+/// events. Thus we may sometimes return up to two events.
+pub fn convert_event(e: Event, window_size: SdlWindowSize) -> ArrayVec {
+ // Translate the coordinates from top-left-origin-with-y-down to centre-origin-with-y-up.
+ // Sdl2 produces input events in pixels, so these positions need to be divided by the width
+ // and height of the window in order to be DPI agnostic.
+ let tx = |x: i32| x as Scalar - window_size.0 as Scalar / 2.0;
+ let ty = |y: i32| -(y as Scalar - window_size.1 as Scalar / 2.0);
+
+ // If the event is converted to a single input, it will stored to this variable `event`
+ // and returned later.
+ // If zero or two events is returned, it is early-returned.
+ // We do so to simplify the code as much as possible.
+ let event = match e {
+ Event::Window { win_event, .. } => match win_event {
+ WindowEvent::Resized(w, h) => Input::Resize(w as Scalar, h as Scalar),
+ WindowEvent::FocusGained => Input::Focus(true),
+ WindowEvent::FocusLost => Input::Focus(false),
+ _ => return ArrayVec::new(),
+ },
+ Event::TextInput { text, .. } => Input::Text(text),
+ Event::KeyDown {
+ keycode: Some(key), ..
+ } => Input::Press(input::Button::Keyboard(convert_key(key))),
+ Event::KeyUp {
+ keycode: Some(key), ..
+ } => Input::Release(input::Button::Keyboard(convert_key(key))),
+ Event::FingerDown { touch_id, x, y, .. } => {
+ let xy = [x as Scalar, y as Scalar];
+ let id = input::touch::Id::new(touch_id as u64);
+ let phase = input::touch::Phase::Start;
+ let touch = input::Touch { phase, id, xy };
+ Input::Touch(touch)
+ }
+ Event::FingerMotion { touch_id, x, y, .. } => {
+ let xy = [x as Scalar, y as Scalar];
+ let id = input::touch::Id::new(touch_id as u64);
+ let phase = input::touch::Phase::Move;
+ let touch = input::Touch { phase, id, xy };
+ Input::Touch(touch)
+ }
+
+ Event::FingerUp { touch_id, x, y, .. } => {
+ let xy = [x as Scalar, y as Scalar];
+ let id = input::touch::Id::new(touch_id as u64);
+ let phase = input::touch::Phase::End;
+ let touch = input::Touch { phase, id, xy };
+ Input::Touch(touch)
+ }
+ Event::MouseMotion {
+ x, y, xrel, yrel, ..
+ } => {
+ let cursor = input::Motion::MouseCursor { x: tx(x), y: ty(y) };
+ let relative = input::Motion::MouseRelative {
+ x: tx(xrel),
+ y: ty(yrel),
+ };
+ return [Input::Motion(cursor), Input::Motion(relative)].into();
+ }
+ Event::MouseWheel { x, y, .. } => {
+ // Invert the scrolling of the *y* axis as *y* is up in conrod.
+ const ARBITRARY_POINTS_PER_LINE_FACTOR: Scalar = 10.0;
+ let x = ARBITRARY_POINTS_PER_LINE_FACTOR * x as Scalar;
+ let y = ARBITRARY_POINTS_PER_LINE_FACTOR * (-y) as Scalar;
+ let motion = input::Motion::Scroll { x, y };
+ Input::Motion(motion)
+ }
+ Event::MouseButtonDown { mouse_btn, .. } => {
+ Input::Press(input::Button::Mouse(convert_mouse_button(mouse_btn)))
+ }
+ Event::MouseButtonUp { mouse_btn, .. } => {
+ Input::Release(input::Button::Mouse(convert_mouse_button(mouse_btn)))
+ }
+ Event::JoyAxisMotion {
+ which,
+ axis_idx,
+ value,
+ ..
+ } => {
+ // Axis motion is an absolute value in the range
+ // [-32768, 32767]. Normalize it down to a float.
+ use std::i16::MAX;
+ let normalized_value = value as f64 / MAX as f64;
+ Input::Motion(input::Motion::ControllerAxis(
+ input::ControllerAxisArgs::new(which, axis_idx, normalized_value),
+ ))
+ }
+ _ => return ArrayVec::new(),
+ };
+ let mut ret = ArrayVec::new();
+ ret.push(event);
+ ret
+}
+
+/// Maps sdl2's key to a conrod `Key`.
+pub fn convert_key(keycode: sdl2::keyboard::Keycode) -> input::keyboard::Key {
+ (keycode as u32).into()
+}
+
+/// Maps sdl2's mouse button to conrod's mouse button.
+pub fn convert_mouse_button(mouse_button: sdl2::mouse::MouseButton) -> input::MouseButton {
+ use conrod_core::input::MouseButton;
+ match mouse_button {
+ sdl2::mouse::MouseButton::Left => MouseButton::Left,
+ sdl2::mouse::MouseButton::Right => MouseButton::Right,
+ sdl2::mouse::MouseButton::Middle => MouseButton::Middle,
+ sdl2::mouse::MouseButton::X1 => MouseButton::X1,
+ sdl2::mouse::MouseButton::X2 => MouseButton::X2,
+ _ => MouseButton::Unknown,
+ }
+}
+
+/// A helper struct that simplifies rendering conrod primitives.
+pub struct Renderer<'font, 'texture> {
+ glyph_cache: GlyphCache<'font>,
+ glyph_texture: Texture<'texture>,
+}
+
+type ImageMap<'t> = conrod_core::image::Map>;
+
+impl<'font, 'texture> Renderer<'font, 'texture> {
+ /// Constructs a new `Renderer`.
+ pub fn new(
+ texture_creator: &'texture TextureCreator,
+ ) -> Result {
+ let [w, h] = conrod_core::mesh::DEFAULT_GLYPH_CACHE_DIMS;
+ Self::with_glyph_cache_dimensions(texture_creator, (w, h))
+ }
+
+ /// Constructs a new `Renderer`.
+ pub fn with_glyph_cache_dimensions(
+ texture_creator: &'texture TextureCreator,
+ glyph_cache_dimensions: (u32, u32),
+ ) -> Result {
+ // Prepare glyph cache
+ let (w, h) = glyph_cache_dimensions;
+ let glyph_cache = GlyphCache::builder().dimensions(w, h).build();
+ let glyph_texture = {
+ let mut texture =
+ texture_creator.create_texture_streaming(PixelFormatEnum::ABGR8888, w, h)?;
+ texture.set_blend_mode(BlendMode::Blend);
+ texture
+ };
+
+ Ok(Self {
+ glyph_cache,
+ glyph_texture,
+ })
+ }
+
+ /// Draws the given primitives.
+ /// It stops drawing as soon as any error occurs.
+ pub fn draw<'m, 't: 'm, I, P>(
+ &mut self,
+ canvas: &mut Canvas,
+ image_map: I,
+ mut primitives: P,
+ ) -> Result<(), DrawPrimitiveError>
+ where
+ I: Into