diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 13f3fde..f806645 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,7 +12,7 @@ jobs: name: Build and test on linux runs-on: ubuntu-latest container: - image: ghcr.io/${{ github.repository_owner }}/lukaj-test:latest + image: ghcr.io/${{ github.repository_owner }}/lukaj-test:develop credentials: username: ${{ github.actor }} password: ${{ secrets.github_token }} @@ -66,6 +66,7 @@ jobs: mingw-w64-x86_64-pkgconf mingw-w64-x86_64-gcc mingw-w64-x86_64-SDL2 + mingw-w64-x86_64-SDL2_ttf - run: rustup update stable-gnu && rustup default stable-gnu - shell: bash run: echo "D:/msys64/mingw64/bin" >> $GITHUB_PATH diff --git a/Cargo.toml b/Cargo.toml index fad25c4..7915f27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ path = "src/lib.rs" clap = { version = "4.4.6", features = ["derive"] } env_logger = "0.10.0" log = "0.4.20" -sdl2 = { version = "0.36.0", default-features = false } +sdl2 = { version = "0.36.0", default-features = false, features = ["ttf"] } cairo-rs = { version = "0.18.2", optional = true } librsvg = { version = "2.57.0", optional = true } resvg = { version = "0.36.0", optional = true } @@ -44,7 +44,7 @@ rgb = "0.8.37" rstest = "0.18.2" [package.metadata.vcpkg] -dependencies = ["sdl2"] +dependencies = ["sdl2", "sdl2-ttf"] git = "https://github.com/microsoft/vcpkg" rev = "a42af01" # release 2023.11.20 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..a04d3a8 --- /dev/null +++ b/build.rs @@ -0,0 +1,7 @@ +fn main() { + // fix for static linking issue (undefined references to 'deflate') + // caused by wrong order of -lz -png (defining group with correct order + // fixes that) + #[cfg(target_os="linux")] + println!("cargo:rustc-link-arg=-Wl,--start-group,-lpng,-lz,--end-group"); +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 715f7b8..53c7a10 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,7 @@ RUN apt-get update \ && apt-get install -y \ automake autoconf build-essential curl cmake git libtool ninja-build tar unzip zip \ libcairo2-dev libgdk-pixbuf-2.0-dev libglib2.0-dev libpango1.0-dev \ - libsdl2-dev libxml2-dev \ + libsdl2-dev libsdl2-ttf-dev libxml2-dev \ xvfb x11-xserver-utils \ && rm -rf /var/lib/apt/lists/* diff --git a/resources/DejaVuSansMono.ttf b/resources/DejaVuSansMono.ttf new file mode 100644 index 0000000..46ea2b7 Binary files /dev/null and b/resources/DejaVuSansMono.ttf differ diff --git a/src/lib.rs b/src/lib.rs index c9ac40b..bc4718b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,9 @@ use sdl2::rect::Rect; use sdl2::render::Canvas; use sdl2::render::Texture; use sdl2::render::TextureCreator; +use sdl2::render::TextureQuery; +use sdl2::rwops::RWops; +use sdl2::ttf::Font; use sdl2::video::Window; use sdl2::video::WindowContext; use sdl2::VideoSubsystem; @@ -196,6 +199,37 @@ trait CanvasEntity { } } +/// Entity of single fixed texture which is always drawn fully +/// (without clipping, stretching etc) to given position +struct SimpleCanvasEntity<'a> { + texture: Texture<'a>, + position: Point, +} + +impl<'a> CanvasEntity for SimpleCanvasEntity<'a> { + fn draw(&self, renderer: &mut sdl2::render::WindowCanvas) -> Result<(), String> { + renderer.copy( + &self.texture, + None, + Rect::new( + self.position.x, + self.position.y, + self.size().0, + self.size().1, + ), + ) + } + + fn size(&self) -> (u32, u32) { + let TextureQuery { width, height, .. } = self.texture.query(); + (width, height) + } + + fn reposition(&mut self, position: Point) { + self.position = position; + } +} + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] enum Side { Left, @@ -456,6 +490,249 @@ impl<'a> CanvasEntity for CheckerBoard<'a> { } } +mod digits_display_module { + use sdl2::pixels::Color; + use sdl2::rect::Point; + use sdl2::rect::Rect; + use sdl2::render::Texture; + use sdl2::render::TextureCreator; + use sdl2::render::TextureQuery; + use sdl2::ttf::Font; + use sdl2::video::WindowContext; + + use super::CanvasEntity; + + fn to_digits(digits: &mut Vec, value: i32) { + digits.clear(); + let mut v = value.abs(); + loop { + let d: u8 = (v % 10) as u8; + v = v / 10; + digits.push(d); + if v == 0 { + break; + }; + } + } + + pub struct DigitsDisplay<'a> { + texture: Texture<'a>, + glyph_width: u32, + glyph_height: u32, + position: Point, + sign: bool, + digits: Vec, + } + + impl<'a> DigitsDisplay<'a> { + pub fn new( + font: &Font, + texture_creator: &'a TextureCreator, + ) -> Result, String> { + let font_surface = font + .render("0123456789-") + .blended(Color::RGBA(0, 0, 0, 255)) + .map_err(|e| e.to_string())?; + + let texture = texture_creator + .create_texture_from_surface(&font_surface) + .map_err(|e| e.to_string())?; + + let TextureQuery { width, height, .. } = texture.query(); + let glyph_width = width / 11; + let glyph_height = height; + + Ok(DigitsDisplay { + texture, + glyph_width, + glyph_height, + position: Point::new(0, 0), + sign: false, + digits: Vec::with_capacity(64), + }) + } + + pub fn update(&mut self, value: i32) { + self.sign = value < 0; + to_digits(&mut self.digits, value); + } + } + + impl<'a> CanvasEntity for DigitsDisplay<'a> { + fn draw(&self, renderer: &mut sdl2::render::WindowCanvas) -> Result<(), String> { + let glyph_width = self.glyph_width; + let glyph_height = self.glyph_height; + + let mut m = 0u32; + + let mut render = |index: u32, position: u32| -> Result<(), String> { + renderer.copy( + &self.texture, + Rect::new((glyph_width * index) as i32, 0, glyph_width, glyph_height), + Rect::new( + self.position.x + (glyph_width * position) as i32, + self.position.y, + glyph_width, + glyph_height, + ), + ) + }; + + if self.sign { + render(10, m)?; + m += 1; + } + + for &digit in self.digits.iter().rev() { + render(digit as u32, m)?; + m += 1; + } + Ok(()) + } + + fn size(&self) -> (u32, u32) { + let TextureQuery { height, .. } = self.texture.query(); + ( + self.glyph_width * (self.digits.len() as u32 + if self.sign { 1 } else { 0 }), + height, + ) + } + + fn reposition(&mut self, position: Point) { + self.position = position; + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn test_to_digits() { + let mut vec = Vec::::with_capacity(64); + to_digits(&mut vec, 123); + assert_eq!(vec, vec![3, 2, 1]); + } + + #[test] + fn test_to_digits_negative() { + let mut vec = Vec::::with_capacity(64); + to_digits(&mut vec, -123); + assert_eq!(vec, vec![3, 2, 1]); + } + } +} + +fn new_static_text<'a>( + text: &str, + font: &Font, + texture_creator: &'a TextureCreator, +) -> Result, String> { + let font_surface = font + .render(text) + .blended(Color::RGBA(0, 0, 0, 255)) + .map_err(|e| e.to_string())?; + + let texture = texture_creator + .create_texture_from_surface(&font_surface) + .map_err(|e| e.to_string())?; + + Ok(SimpleCanvasEntity { + texture, + position: Point::new(0, 0), + }) +} + +struct LabeledDigitsDisplay<'a> { + label: SimpleCanvasEntity<'a>, + digits: digits_display_module::DigitsDisplay<'a>, +} + +impl<'a> LabeledDigitsDisplay<'a> { + fn new( + text: &str, + font: &Font, + texture_creator: &'a TextureCreator, + ) -> Result, String> { + let label = new_static_text(text, &font, &texture_creator)?; + let digits = digits_display_module::DigitsDisplay::new(&font, &texture_creator)?; + Ok(LabeledDigitsDisplay { label, digits }) + } + + fn update(&mut self, value: i32) { + self.digits.update(value) + } +} + +impl<'a> CanvasEntity for LabeledDigitsDisplay<'a> { + fn draw(&self, renderer: &mut sdl2::render::WindowCanvas) -> Result<(), String> { + self.label.draw(renderer)?; + self.digits.draw(renderer)?; + Ok(()) + } + + fn reposition(&mut self, position: Point) { + self.label.reposition(position); + self.digits + .reposition(position + Point::new(self.label.size().0 as i32, 0)); + } + + fn size(&self) -> (u32, u32) { + ( + self.label.size().0 + self.digits.size().0, + self.label.size().1, + ) + } +} + +struct StatusBar<'a> { + mouse_x_display: LabeledDigitsDisplay<'a>, + mouse_y_display: LabeledDigitsDisplay<'a>, +} + +impl<'a> StatusBar<'a> { + fn new( + font: &Font, + texture_creator: &'a TextureCreator, + ) -> Result, String> { + Ok(StatusBar { + mouse_x_display: LabeledDigitsDisplay::new("x:", &font, &texture_creator)?, + mouse_y_display: LabeledDigitsDisplay::new(" y:", &font, &texture_creator)?, + }) + } + + fn update(&mut self, x: i32, y: i32) { + self.mouse_x_display.update(x); + self.mouse_y_display.update(y); + } +} + +impl<'a> CanvasEntity for StatusBar<'a> { + fn draw(&self, renderer: &mut sdl2::render::WindowCanvas) -> Result<(), String> { + self.mouse_x_display.draw(renderer)?; + self.mouse_y_display.draw(renderer)?; + Ok(()) + } + + fn reposition(&mut self, position: Point) { + fn reposition_internal(element: &mut T, position: Point) -> Point { + element.reposition(position); + position + Point::new(element.size().0 as i32, 0) + } + + let mut p = position; + p = reposition_internal(&mut self.mouse_x_display, p); + _ = reposition_internal(&mut self.mouse_y_display, p); + } + + fn size(&self) -> (u32, u32) { + ( + self.mouse_x_display.size().0 + self.mouse_y_display.size().0, + self.mouse_x_display.size().1, + ) + } +} + fn get_texture_builder<'a, P: AsRef>( path: P, backend: SvgBackend, @@ -615,6 +892,11 @@ pub fn app>( let sdl_context = sdl2::init()?; let video_subsystem = sdl_context.video()?; + let ttf_context = sdl2::ttf::init().map_err(|e| e.to_string())?; + + // Load a font TODO: not sure if needed, perhaps will load with fontdb only + let font = include_bytes!("../resources/DejaVuSansMono.ttf"); + let font = &ttf_context.load_font_from_rwops(RWops::from_bytes(font)?, 16)?; let min_size: (u32, u32) = (800, 600); let max_size: (u32, u32) = get_max_window_size(&video_subsystem)?; @@ -671,13 +953,17 @@ pub fn app>( )); } + // canvas elements: let left = left_svg.rasterize(&texture_creator, scale)?; let right = right_svg.rasterize(&texture_creator, scale)?; - - let mut drag = drag_module::Drag::new(); let mut diff = Diff::new(left, right); let mut workarea = CheckerBoard::new(&texture_creator, diff.size())?; + let mut status_bar = StatusBar::new(&font, &texture_creator)?; + status_bar.reposition(canvas.viewport().bottom_left()); + + // app logic handling: + let mut drag = drag_module::Drag::new(); let mut event_pump = sdl_context.event_pump()?; 'running: loop { @@ -686,7 +972,8 @@ pub fn app>( canvas.set_draw_color(Color::RGBA(255, 255, 255, 255)); canvas.clear(); - let mut center = canvas.viewport().center(); + let viewport = canvas.viewport(); + let mut center = viewport.center(); for event in event_pump.poll_iter() { match event { @@ -746,10 +1033,17 @@ pub fn app>( diff.update(&mouse_state); diff.draw(&mut canvas)?; + status_bar.reposition(viewport.bottom_left() - Point::new(0, status_bar.size().1 as i32)); + status_bar.update( + mouse_state.x() - workarea.position.x(), + mouse_state.y() - workarea.position.y(), + ); + status_bar.draw(&mut canvas)?; + canvas.present(); let frame_duration = frame_start.elapsed().as_micros() as u64; - trace!("Frame duration: {}us", frame_duration); + trace!("Frame duration average: {}us", frame_duration); match testing { Some(val) => {