From d3b769d1bcec5a0581d7da6db241ccede7bb1270 Mon Sep 17 00:00:00 2001 From: penguindark <57967770+penguindark@users.noreply.github.com> Date: Sat, 11 Dec 2021 21:18:03 +0100 Subject: [PATCH] examples: add an image viewer program (#12797) --- examples/viewer/LICENSE | 21 + examples/viewer/README.md | 54 +++ examples/viewer/file_scan.v | 343 +++++++++++++ examples/viewer/v.mod | 7 + examples/viewer/view.v | 832 ++++++++++++++++++++++++++++++++ examples/viewer/zip_container.v | 71 +++ 6 files changed, 1328 insertions(+) create mode 100644 examples/viewer/LICENSE create mode 100644 examples/viewer/README.md create mode 100644 examples/viewer/file_scan.v create mode 100644 examples/viewer/v.mod create mode 100644 examples/viewer/view.v create mode 100644 examples/viewer/zip_container.v diff --git a/examples/viewer/LICENSE b/examples/viewer/LICENSE new file mode 100644 index 000000000..3a615dd13 --- /dev/null +++ b/examples/viewer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Dario Deledda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/examples/viewer/README.md b/examples/viewer/README.md new file mode 100644 index 000000000..ba4b5b212 --- /dev/null +++ b/examples/viewer/README.md @@ -0,0 +1,54 @@ +# vviewer +Image viewer for V + +This is an example of a simple image viewer written in V. + +## Usage + +The program can be invoked by the command line: + +file list: `viewer img1.jpg inmg2.bmp img3.tga` +folder list: `viewer folder1 folder2` +zip list: `viewer folder1.zip folder2.zip` + +All folders/zips are scanned for images. +The user can mix files, folders, and zips. + +mixed list: `viewer img1.jpg img2.bmp folder1 folder2 img2.tga folder1.zip` + +## Interactive usage + +Run the viewer then drag and drop files,folders and zips on it. + +## Accepted image format + +JPEG, PNG, BMP, PSD, TGA, GIF (not animated), HDR, PIC, PNM + +#### Functions +The user can navigate through the files passed to the viewer. +The following operations can be performed on each image: + +- **Pan**, move over the image +- **Zoom**, magnify or reduce the image +- **Rotate**, rotate by 90 degree steps + +## Key bindings + +**H** - show this help + +**ESC/q** - Quit +**cursor right** - Next image +**cursor left** - Previous image +**cursor up** - Next folder +**cursor down** - Previous folder +**F** - Toggle full screen +**R** - Rotate image of 90 degree +**I** - Toggle the info text + +**mouse wheel** - next/previous images +Hold **left Mouse button** - Pan on the image +Hold **right Mouse button** - Zoom on the image + +#### Author: + +Dario Deledda 2021 (c) \ No newline at end of file diff --git a/examples/viewer/file_scan.v b/examples/viewer/file_scan.v new file mode 100644 index 000000000..da84fc137 --- /dev/null +++ b/examples/viewer/file_scan.v @@ -0,0 +1,343 @@ +/********************************************************************** +* +* File scanner +* +* Copyright (c) 2021 Dario Deledda. All rights reserved. +* Use of this source code is governed by an MIT license +* that can be found in the LICENSE file. +* +* TODO: +**********************************************************************/ +import os + +// STBI supported format +// STBI_NO_JPEG * +// STBI_NO_PNG * +// STBI_NO_BMP * +// STBI_NO_PSD +// STBI_NO_TGA * +// STBI_NO_GIF * +// STBI_NO_HDR * +// STBI_NO_PIC * +// STBI_NO_PNM * + +/****************************************************************************** +* +* Struct and Enums +* +******************************************************************************/ +enum Item_type { + file = 0 + folder + // archive format + zip = 16 + archive_file + // graphic format, MUST stay after the other types!! + bmp = 32 + jpg + png + gif + tga + ppm + pgm + pic + hdr +} + +pub struct Item { +pub mut: + path string + name string + size u64 + i_type Item_type = .file + container_index int // used if the item is in a container (.zip, .rar, etc) + container_item_index int // index in the container if the item is contained + need_extract bool // if true need to extraction from the container + drawable bool // if true the image can be showed + n_item int // + rotation int // number of rotation of PI/2 +} + +struct Item_list { +pub mut: + lst []Item + path_sep string + item_index int = -1 // image currently shown + n_item int // number of images scanned + loaded bool // flag that indicate that the list is ready to be used +} + +/****************************************************************************** +* +* Utility functions +* +******************************************************************************/ +[inline] +fn modulo(x int, n int) int { + return (x % n + n) % n +} + +[inline] +fn get_extension(x string) Item_type { + // 4 char extension check + if x.len > 4 { + ext4 := x[x.len - 4..].to_lower() + match ext4 { + // containers + '.zip' { return .zip } + // graphic formats + '.jpg' { return .jpg } + '.png' { return .png } + '.bmp' { return .bmp } + '.gif' { return .gif } + '.tga' { return .tga } + '.ppm' { return .ppm } + '.pgm' { return .pgm } + '.pic' { return .pic } + '.hdr' { return .hdr } + else {} + } + } + // 5 char extension check + if x.len > 5 { + ext5 := x[x.len - 5..].to_lower() + if ext5 == '.jpeg' { + { + return .jpg + } + } + } + return .file +} + +[inline] +fn is_image(x Item_type) bool { + if int(x) >= int(Item_type.bmp) { + return true + } + return false +} + +[inline] +fn is_container(x Item_type) bool { + if x in [.zip, .folder] { + return true + } + return false +} + +[inline] +fn (item_list Item_list) is_inside_a_container() bool { + if item_list.lst.len <= 0 || item_list.n_item <= 0 { + return false + } + return item_list.lst[item_list.item_index].need_extract +} + +[inline] +fn (item_list Item_list) get_file_path() string { + if item_list.lst.len <= 0 || item_list.n_item <= 0 { + return '' + } + if item_list.lst[item_list.item_index].path.len > 0 { + return '${item_list.lst[item_list.item_index].path}$item_list.path_sep${item_list.lst[item_list.item_index].name}' + } + return item_list.lst[item_list.item_index].name +} + +/****************************************************************************** +* +* Scan functions +* +******************************************************************************/ +fn (mut item_list Item_list) scan_folder(path string, in_index int) ? { + println('Scanning [$path]') + mut folder_list := []string{} + lst := os.ls(path) ? + + // manage the single files + for c, x in lst { + pt := '$path$item_list.path_sep$x' + mut item := Item{ + path: path + name: x + container_index: in_index + container_item_index: c + } + if os.is_dir(pt) { + folder_list << x + } else { + ext := get_extension(x) + if ext == .zip { + item.i_type = .zip + item_list.lst << item + item_list.scan_zip(pt, item_list.lst.len - 1) ? + continue + } + if is_image(ext) == true { + item_list.n_item += 1 + item.n_item = item_list.n_item + item.i_type = ext + item.drawable = true + item_list.lst << item + continue + } + } + } + + // manage the folders + for x in folder_list { + pt := '$path$item_list.path_sep$x' + item := Item{ + path: path + name: x + i_type: .folder + } + item_list.lst << item + item_list.scan_folder(pt, item_list.lst.len - 1) ? + } + // println(item_list.lst.len) + // println("==================================") +} + +fn (item_list Item_list) print_list() { + println('================================') + for x in item_list.lst { + if x.i_type == .folder { + print('[]') + } + if x.i_type == .zip { + print('[ZIP]') + } + println('$x.path => $x.container_index $x.container_item_index $x.name ne:$x.need_extract') + } + println('n_item: $item_list.n_item index: $item_list.item_index') + println('================================') +} + +fn (mut item_list Item_list) get_items_list(args []string) { + item_list.loaded = false + println('Args: $args') + + item_list.path_sep = $if windows { '\\' } $else { '/' } + for x in args { + // scan folder + if os.is_dir(x) { + mut item := Item{ + path: x + name: x + container_index: item_list.lst.len + i_type: .folder + } + item_list.lst << item + item_list.scan_folder(x, item_list.lst.len - 1) or { + eprintln('ERROR: scanning folder [$x]!') + continue + } + } else { + mut item := Item{ + path: '' + name: x + container_index: -1 + } + ext := get_extension(x) + // scan .zip + if ext == .zip { + item.i_type = .zip + item_list.lst << item + item_list.scan_zip(x, item_list.lst.len - 1) or { + eprintln('ERROR: scanning zip [$x]!') + continue + } + continue + } + // single images + if is_image(ext) == true { + item_list.n_item += 1 + item.n_item = item_list.n_item + item.i_type = ext + item.drawable = true + item_list.lst << item + continue + } + } + } + // debug call for list all the loaded items + // item_list.print_list() + + println('Items: $item_list.n_item') + println('Scanning done.') + + item_list.get_next_item(1) + item_list.loaded = true +} + +/****************************************************************************** +* +* Navigation functions +* +******************************************************************************/ +fn (mut item_list Item_list) get_next_item(in_inc int) { + // if empty exit + if item_list.lst.len <= 0 || item_list.n_item <= 0 { + return + } + + inc := if in_inc > 0 { 1 } else { -1 } + mut i := item_list.item_index + in_inc + i = modulo(i, item_list.lst.len) + start := i + for { + if item_list.lst[i].drawable == true { + item_list.item_index = i + break + } + i = i + inc + i = modulo(i, item_list.lst.len) + // if we are in a loop break it + if i == start { + break + } + } + // println("Found: ${item_list.item_index}") +} + +fn (mut item_list Item_list) go_to_next_container(in_inc int) { + // if empty exit + if item_list.lst.len <= 0 || item_list.n_item <= 0 { + return + } + inc := if in_inc > 0 { 1 } else { -1 } + mut i := item_list.item_index + in_inc + i = modulo(i, item_list.lst.len) + start := i + for { + // check if we found a folder + if is_container(item_list.lst[i].i_type) == true + && i != item_list.lst[item_list.item_index].container_index { + item_list.item_index = i + item_list.get_next_item(1) + break + } + // continue to search + i = i + inc + i = modulo(i, item_list.lst.len) + // if we are in a loop break it + if i == start { + break + } + } +} + +/****************************************************************************** +* +* Other functions +* +******************************************************************************/ +[inline] +fn (mut item_list Item_list) rotate(in_inc int) { + item_list.lst[item_list.item_index].rotation += in_inc + if item_list.lst[item_list.item_index].rotation >= 4 { + item_list.lst[item_list.item_index].rotation = 0 + } +} diff --git a/examples/viewer/v.mod b/examples/viewer/v.mod new file mode 100644 index 000000000..72044f7c8 --- /dev/null +++ b/examples/viewer/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'vviewer', + description: 'A simple image viewer written in V.', + version: '0.9', + repo_url: 'https://github.com/vlang/v/tree/master/examples/viewer', + dependencies: [] +} diff --git a/examples/viewer/view.v b/examples/viewer/view.v new file mode 100644 index 000000000..cce8f951c --- /dev/null +++ b/examples/viewer/view.v @@ -0,0 +1,832 @@ +/********************************************************************** +* +* simple Picture Viewer V. 0.9 +* +* Copyright (c) 2021 Dario Deledda. All rights reserved. +* Use of this source code is governed by an MIT license +* that can be found in the LICENSE file. +* +* TODO: +* - add an example with shaders +**********************************************************************/ +import os +import gg +import gx +import sokol.gfx +import sokol.sgl +import sokol.sapp +import stbi +import szip +import strings + +// Help text +const ( + help_text_rows = [ + 'Image Viwer 0.9 help.', + '', + 'ESC/q - Quit', + 'cur. right - Next image', + 'cur. left - Previous image', + 'cur. up - Next folder', + 'cur. down - Previous folder', + 'F - Toggle full screen', + 'R - Rotate image of 90 degree', + 'I - Toggle the info text', + '', + 'mouse wheel - next/previous images', + 'keep pressed left Mouse button - Pan on the image', + 'keep pressed rigth Mouse button - Zoom on the image', + ] +) + +const ( + win_width = 800 + win_height = 800 + bg_color = gx.black + pi_2 = 3.14159265359 / 2.0 + uv = [f32(0), 0, 1, 0, 1, 1, 0, 1]! // used for zoom icon during rotations + + text_drop_files = 'Drop here some images/folder/zip to navigate in the pics' + text_scanning = 'Scanning...' + text_loading = 'Loading...' +) + +enum Viewer_state { + loading + scanning + show + error +} + +struct App { +mut: + gg &gg.Context + pip_viewer C.sgl_pipeline + texture C.sg_image + init_flag bool + frame_count int + mouse_x int = -1 + mouse_y int = -1 + scroll_y int + + state Viewer_state = .scanning + // translation + tr_flag bool + tr_x f32 = 0.0 + tr_y f32 = 0.0 + last_tr_x f32 = 0.0 + last_tr_y f32 = 0.0 + // scaling + sc_flag bool + scale f32 = 1.0 + sc_x f32 = 0.0 + sc_y f32 = 0.0 + last_sc_x f32 = 0.0 + last_sc_y f32 = 0.0 + // loaded image + img_w int + img_h int + img_ratio f32 = 1.0 + // item list + item_list &Item_list + // Text info and help + show_info_flag bool = true + show_help_flag bool + // zip container + zip &szip.Zip // pointer to the szip structure + zip_index int = -1 // index of the zip contaire item + // memory buffer + mem_buf voidptr // buffer used to load items from files/containers + mem_buf_size int // size of the buffer + // font + font_path string // path to the temp font file + // logo + logo_path string // path of the temp font logo + logo_texture C.sg_image + logo_w int + logo_h int + logo_ratio f32 = 1.0 + // string builder + bl strings.Builder = strings.new_builder(512) +} + +/****************************************************************************** +* +* Texture functions +* +******************************************************************************/ +fn create_texture(w int, h int, buf &u8) C.sg_image { + sz := w * h * 4 + mut img_desc := C.sg_image_desc{ + width: w + height: h + num_mipmaps: 0 + min_filter: .linear + mag_filter: .linear + // usage: .dynamic + wrap_u: .clamp_to_edge + wrap_v: .clamp_to_edge + label: &byte(0) + d3d11_texture: 0 + } + // comment if .dynamic is enabled + img_desc.data.subimage[0][0] = C.sg_range{ + ptr: buf + size: usize(sz) + } + + sg_img := C.sg_make_image(&img_desc) + return sg_img +} + +fn destroy_texture(sg_img C.sg_image) { + C.sg_destroy_image(sg_img) +} + +// Use only if: .dynamic is enabled +fn update_text_texture(sg_img C.sg_image, w int, h int, buf &byte) { + sz := w * h * 4 + mut tmp_sbc := C.sg_image_data{} + tmp_sbc.subimage[0][0] = C.sg_range{ + ptr: buf + size: usize(sz) + } + C.sg_update_image(sg_img, &tmp_sbc) +} + +/****************************************************************************** +* +* Memory buffer +* +******************************************************************************/ +[inline] +fn (mut app App) resize_buf_if_needed(in_size int) { + // manage the memory buffer + if app.mem_buf_size < in_size { + println('Managing FILE memory buffer, allocated [$in_size]Bytes') + // free previous buffer if any exist + if app.mem_buf_size > 0 { + unsafe { + free(app.mem_buf) + } + } + // allocate the memory + unsafe { + app.mem_buf = malloc(int(in_size)) + app.mem_buf_size = int(in_size) + } + } +} + +/****************************************************************************** +* +* Loading functions +* +******************************************************************************/ +// read_bytes from file in `path` in the memory buffer of app. +[manualfree] +fn (mut app App) read_bytes(path string) bool { + mut fp := os.vfopen(path, 'rb') or { + eprintln('ERROR: Can not open the file [$path].') + return false + } + defer { + C.fclose(fp) + } + cseek := C.fseek(fp, 0, C.SEEK_END) + if cseek != 0 { + eprintln('ERROR: Can not seek in the file [$path].') + return false + } + fsize := C.ftell(fp) + if fsize < 0 { + eprintln('ERROR: File [$path] has size is 0.') + return false + } + C.rewind(fp) + + app.resize_buf_if_needed(int(fsize)) + + nr_read_elements := int(C.fread(app.mem_buf, fsize, 1, fp)) + if nr_read_elements == 0 && fsize > 0 { + eprintln('ERROR: Can not read the file [$path] in the memory buffer.') + return false + } + return true +} + +// read a file as []byte +pub fn read_bytes_from_file(file_path string) []byte { + mut buffer := []byte{} + buffer = os.read_bytes(file_path) or { + eprintln('ERROR: Texure file: [$file_path] NOT FOUND.') + exit(0) + } + return buffer +} + +fn (mut app App) load_texture_from_buffer(buf voidptr, buf_len int) (C.sg_image, int, int) { + // load image + stbi.set_flip_vertically_on_load(true) + img := stbi.load_from_memory(buf, buf_len) or { + eprintln('ERROR: Can not load image from buffer, file: [${app.item_list.lst[app.item_list.item_index]}].') + return app.logo_texture, app.logo_w, app.logo_h + // exit(1) + } + res := create_texture(int(img.width), int(img.height), img.data) + unsafe { + img.free() + } + return res, int(img.width), int(img.height) +} + +pub fn (mut app App) load_texture_from_file(file_name string) (C.sg_image, int, int) { + app.read_bytes(file_name) + return app.load_texture_from_buffer(app.mem_buf, app.mem_buf_size) +} + +pub fn show_logo(mut app App) { + clear_modifier_params(mut app) + if app.texture != app.logo_texture { + destroy_texture(app.texture) + } + app.texture = app.logo_texture + app.img_w = app.logo_w + app.img_h = app.logo_h + app.img_ratio = f32(app.img_w) / f32(app.img_h) + // app.gg.refresh_ui() +} + +pub fn load_image(mut app App) { + if app.item_list.loaded == false || app.init_flag == false { + // show_logo(mut app) + // app.state = .show + return + } + app.state = .loading + clear_modifier_params(mut app) + // destroy the texture, avoid to destroy the logo + if app.texture != app.logo_texture { + destroy_texture(app.texture) + } + + // load from .ZIP file + if app.item_list.is_inside_a_container() == true { + app.texture, app.img_w, app.img_h = app.load_texture_from_zip() or { + eprintln('ERROR: Can not load image from .ZIP file [${app.item_list.lst[app.item_list.item_index]}].') + show_logo(mut app) + app.state = .show + return + } + app.img_ratio = f32(app.img_w) / f32(app.img_h) + app.state = .show + // app.gg.refresh_ui() + return + } + + // if we are out of the zip, close it + if app.zip_index >= 0 { + app.zip_index = -1 + app.zip.close() + } + + file_path := app.item_list.get_file_path() + if file_path.len > 0 { + // println("${app.item_list.lst[app.item_list.item_index]} $file_path ${app.item_list.lst.len}") + app.texture, app.img_w, app.img_h = app.load_texture_from_file(file_path) + app.img_ratio = f32(app.img_w) / f32(app.img_h) + // println("texture: [${app.img_w},${app.img_h}] ratio: ${app.img_ratio}") + } else { + app.texture = app.logo_texture + app.img_w = app.logo_w + app.img_h = app.logo_h + app.img_ratio = f32(app.img_w) / f32(app.img_h) + println('texture NOT FOUND: use logo!') + } + app.state = .show +} + +/****************************************************************************** +* +* Init / Cleanup +* +******************************************************************************/ +fn app_init(mut app App) { + app.init_flag = true + + // 3d pipeline + mut pipdesc := C.sg_pipeline_desc{} + unsafe { C.memset(&pipdesc, 0, sizeof(pipdesc)) } + + color_state := C.sg_color_state{ + blend: C.sg_blend_state{ + enabled: true + src_factor_rgb: gfx.BlendFactor(C.SG_BLENDFACTOR_SRC_ALPHA) + dst_factor_rgb: gfx.BlendFactor(C.SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA) + } + } + pipdesc.colors[0] = color_state + + pipdesc.depth = C.sg_depth_state{ + write_enabled: true + compare: gfx.CompareFunc(C.SG_COMPAREFUNC_LESS_EQUAL) + } + pipdesc.cull_mode = .back + app.pip_viewer = sgl.make_pipeline(&pipdesc) + + // load logo + app.logo_texture, app.logo_w, app.logo_h = app.load_texture_from_file(app.logo_path) + app.logo_ratio = f32(app.img_w) / f32(app.img_h) + + app.img_w = app.logo_w + app.img_h = app.logo_h + app.img_ratio = app.logo_ratio + app.texture = app.logo_texture + + println('INIT DONE!') + + // init done, load the first image if any + load_image(mut app) +} + +fn cleanup(mut app App) { + gfx.shutdown() + + // delete temp files + os.rm(app.font_path) or { eprintln('ERROR: Can not delete temp font file.') } + os.rm(app.logo_path) or { eprintln('ERROR: Can not delete temp logo file.') } + println('Cleaning done.') +} + +/****************************************************************************** +* +* Draw functions +* +******************************************************************************/ +[manualfree] +fn frame(mut app App) { + ws := gg.window_size_real_pixels() + if ws.width <= 0 || ws.height <= 0 { + return + } + + mut ratio := f32(ws.width) / ws.height + dw := ws.width + dh := ws.height + + app.gg.begin() + sgl.defaults() + + // set viewport + sgl.viewport(0, 0, dw, dh, true) + + // enable our pipeline + sgl.load_pipeline(app.pip_viewer) + sgl.enable_texture() + sgl.texture(app.texture) + + // translation + tr_x := app.tr_x / app.img_w + tr_y := -app.tr_y / app.img_h + sgl.push_matrix() + sgl.translate(tr_x, tr_y, 0.0) + // scaling/zoom + sgl.scale(2.0 * app.scale, 2.0 * app.scale, 0.0) + // roation + mut rotation := 0 + if app.state == .show && app.item_list.n_item > 0 { + rotation = app.item_list.lst[app.item_list.item_index].rotation + sgl.rotate(pi_2 * f32(rotation), 0.0, 0.0, -1.0) + } + + // draw the image + mut w := f32(0.5) + mut h := f32(0.5) + + // for 90 and 270 degree invert w and h + // rotation change image ratio, manage it + if rotation & 1 == 1 { + tmp := w + w = h + h = tmp + h /= app.img_ratio * ratio + } else { + h /= app.img_ratio / ratio + } + + // manage image overflow in case of strange scales + if h > 0.5 { + reduction_factor := 0.5 / h + h = h * reduction_factor + w = w * reduction_factor + } + if w > 0.5 { + reduction_factor := 0.5 / w + h = h * reduction_factor + w = w * reduction_factor + } + + // println("$w,$h") + // white multiplicator for now + mut c := [byte(255), 255, 255]! + sgl.begin_quads() + sgl.v2f_t2f_c3b(-w, -h, 0, 0, c[0], c[1], c[2]) + sgl.v2f_t2f_c3b(w, -h, 1, 0, c[0], c[1], c[2]) + sgl.v2f_t2f_c3b(w, h, 1, 1, c[0], c[1], c[2]) + sgl.v2f_t2f_c3b(-w, h, 0, 1, c[0], c[1], c[2]) + sgl.end() + + // restore all the transformations + sgl.pop_matrix() + + // Zoom icon + /* + if app.show_info_flag == true && app.scale > 1 { + mut bw := f32(0.25) + mut bh := f32(0.25 / app.img_ratio) + + // manage the rotations + if rotation & 1 == 1 { + bw,bh = bh,bw + } + mut bx := f32(1 - bw) + mut by := f32(1 - bh) + if rotation & 1 == 1 { + bx,by = by,bx + } + + bh_old1 := bh + bh *= ratio + by += (bh_old1 - bh) + + // draw the zoom icon + sgl.begin_quads() + r := int(u32(rotation) << 1) + sgl.v2f_t2f_c3b(bx , by , uv[(0 + r) & 7] , uv[(1 + r) & 7], c[0], c[1], c[2]) + sgl.v2f_t2f_c3b(bx + bw, by , uv[(2 + r) & 7] , uv[(3 + r) & 7], c[0], c[1], c[2]) + sgl.v2f_t2f_c3b(bx + bw, by + bh, uv[(4 + r) & 7] , uv[(5 + r) & 7], c[0], c[1], c[2]) + sgl.v2f_t2f_c3b(bx , by + bh, uv[(6 + r) & 7] , uv[(7 + r) & 7], c[0], c[1], c[2]) + sgl.end() + + // draw the zoom rectangle + sgl.disable_texture() + + bw_old := bw + bh_old := bh + bw /= app.scale + bh /= app.scale + bx += (bw_old - bw) / 2 - (tr_x / 8) / app.scale + by += (bh_old - bh) / 2 - ((tr_y / 8) / app.scale) * ratio + + c = [byte(255),255,0]! // yellow + sgl.begin_line_strip() + sgl.v2f_c3b(bx , by , c[0], c[1], c[2]) + sgl.v2f_c3b(bx + bw, by , c[0], c[1], c[2]) + sgl.v2f_c3b(bx + bw, by + bh, c[0], c[1], c[2]) + sgl.v2f_c3b(bx , by + bh, c[0], c[1], c[2]) + sgl.v2f_c3b(bx , by , c[0], c[1], c[2]) + sgl.end() + } + */ + sgl.disable_texture() + + // + // Draw info text + // + x := 10 + y := 10 + + app.gg.begin() + + if app.state in [.scanning, .loading] { + if app.state == .scanning { + draw_text(mut app, text_scanning, x, y, 20) + } else { + draw_text(mut app, text_loading, x, y, 20) + } + } else if app.state == .show { + // print the info text if needed + if app.item_list.n_item > 0 && app.show_info_flag == true { + /* + // waiting for better autofree + num := app.item_list.lst[app.item_list.item_index].n_item + of_num := app.item_list.n_item + x_screen := int(w*2*app.scale*dw) + y_screen := int(h*2*app.scale*dw) + rotation_angle := 90 * rotation + scale_str := "${app.scale:.2}" + text := "${num}/${of_num} [${app.img_w},${app.img_h}]=>[${x_screen},${y_screen}] ${app.item_list.lst[app.item_list.item_index].name} scale: ${scale_str} rotation: ${rotation_angle}" + //text := "${num}/${of_num}" + draw_text(mut app, text, 10, 10, 20) + unsafe{ + text.free() + } + */ + + // Using string builder to avoid memory leak + num := app.item_list.lst[app.item_list.item_index].n_item + of_num := app.item_list.n_item + x_screen := int(w * 2 * app.scale * dw) + y_screen := int(h * 2 * app.scale * dw) + rotation_angle := 90 * rotation + scale_str := '${app.scale:.2}' + app.bl.clear() + app.bl.write_string('$num/$of_num') + app.bl.write_string(' [${app.img_w}x$app.img_h]=>[${x_screen}x$y_screen]') + app.bl.write_string(' ${app.item_list.lst[app.item_list.item_index].name}') + app.bl.write_string(' scale: $scale_str rotation: $rotation_angle') + draw_text(mut app, app.bl.str(), 10, 10, 20) + } else { + if app.item_list.n_item <= 0 { + draw_text(mut app, text_drop_files, 10, 10, 20) + } + } + } + + // + // Draw Help text + // + if app.show_help_flag == true { + mut txt_y := 30 + for r in help_text_rows { + draw_text(mut app, r, 10, txt_y, 20) + txt_y += 20 + } + } + + app.gg.end() + app.frame_count++ +} + +// draw readable text +fn draw_text(mut app App, in_txt string, in_x int, in_y int, fnt_sz f32) { + scale := app.gg.scale + font_size := int(fnt_sz * scale) + + mut txt_conf_c0 := gx.TextCfg{ + color: gx.white // gx.rgb( (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff) + align: .left + size: font_size + } + mut txt_conf_c1 := gx.TextCfg{ + color: gx.black // gx.rgb( (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff) + align: .left + size: font_size + } + + x := int(in_x * scale) + y := int(in_y * scale) + app.gg.draw_text(x + 2, y + 2, in_txt, txt_conf_c0) + app.gg.draw_text(x, y, in_txt, txt_conf_c1) +} + +/****************************************************************************** +* +* events management +* +******************************************************************************/ +fn clear_modifier_params(mut app App) { + app.scale = 1.0 + + app.sc_flag = false + app.sc_x = 0 + app.sc_y = 0 + app.last_sc_x = 0 + app.last_sc_y = 0 + + app.tr_flag = false + app.tr_x = 0 + app.tr_y = 0 + app.last_tr_x = 0 + app.last_tr_y = 0 +} + +fn my_event_manager(mut ev gg.Event, mut app App) { + // navigation using the mouse wheel + app.scroll_y = int(ev.scroll_y) + if app.scroll_y != 0 { + inc := int(-1 * app.scroll_y / 4) + if app.item_list.n_item > 0 { + app.item_list.get_next_item(inc) + load_image(mut app) + } + } + + if ev.typ == .mouse_move { + app.mouse_x = int(ev.mouse_x) + app.mouse_y = int(ev.mouse_y) + } + if ev.typ == .touches_began || ev.typ == .touches_moved { + if ev.num_touches > 0 { + touch_point := ev.touches[0] + app.mouse_x = int(touch_point.pos_x) + app.mouse_y = int(touch_point.pos_y) + } + } + + // clear all parameters + if ev.typ == .mouse_down && ev.mouse_button == .middle { + clear_modifier_params(mut app) + } + + // ws := gg.window_size_real_pixels() + // ratio := f32(ws.width) / ws.height + // dw := ws.width + // dh := ws.height + + // --- translate --- + if ev.typ == .mouse_down && ev.mouse_button == .left { + app.tr_flag = true + app.last_tr_x = app.mouse_x + app.last_tr_y = app.mouse_y + } + if ev.typ == .mouse_up && ev.mouse_button == .left && app.tr_flag == true { + app.tr_flag = false + } + if ev.typ == .mouse_move && app.tr_flag == true { + app.tr_x += (app.mouse_x - app.last_tr_x) * 3 * app.gg.scale + app.tr_y += (app.mouse_y - app.last_tr_y) * 3 * app.gg.scale + app.last_tr_x = app.mouse_x + app.last_tr_y = app.mouse_y + // println("Translate: ${app.tr_x} ${app.tr_y}") + } + + // --- scaling --- + if ev.typ == .mouse_down && ev.mouse_button == .right && app.sc_flag == false { + app.sc_flag = true + app.last_sc_x = app.mouse_x + app.last_sc_y = app.mouse_y + } + if ev.typ == .mouse_up && ev.mouse_button == .right && app.sc_flag == true { + app.sc_flag = false + } + if ev.typ == .mouse_move && app.sc_flag == true { + app.sc_x = app.mouse_x - app.last_sc_x + app.sc_y = app.mouse_y - app.last_sc_y + app.last_sc_x = app.mouse_x + app.last_sc_y = app.mouse_y + + app.scale += f32(app.sc_x / 100) + if app.scale < 0.1 { + app.scale = 0.1 + } + if app.scale > 32 { + app.scale = 32 + } + } + + if ev.typ == .key_down { + // println(ev.key_code) + + // Exit using the ESC key or Q key + if ev.key_code == .escape || ev.key_code == .q { + cleanup(mut app) + exit(0) + } + // Toggle info text OSD + if ev.key_code == .i { + app.show_info_flag = !app.show_info_flag + } + // Toggle help text + if ev.key_code == .h { + app.show_help_flag = !app.show_help_flag + } + + // do actions only if there are items in the list + if app.item_list.loaded == true && app.item_list.n_item > 0 { + // show previous image + if ev.key_code == .left { + app.item_list.get_next_item(-1) + load_image(mut app) + } + // show next image + if ev.key_code == .right { + app.item_list.get_next_item(1) + load_image(mut app) + } + + // jump to the next container if possible + if ev.key_code == .up { + app.item_list.go_to_next_container(1) + load_image(mut app) + } + // jump to the previous container if possible + if ev.key_code == .down { + app.item_list.go_to_next_container(-1) + load_image(mut app) + } + + // rotate the image + if ev.key_code == .r { + app.item_list.rotate(1) + } + + // full screen + if ev.key_code == .f { + println('Full screen state: $sapp.is_fullscreen()') + sapp.toggle_fullscreen() + } + } + } + + // drag&drop + if ev.typ == .files_droped { + app.state = .scanning + // set logo texture during scanning + show_logo(mut app) + + num := sapp.get_num_dropped_files() + mut file_list := []string{} + for i in 0 .. num { + file_list << sapp.get_dropped_file_path(i) + } + println('Scanning: $file_list') + app.item_list = &Item_list{} + app.item_list.loaded = false + + // load_image(mut app) + // go app.item_list.get_items_list(file_list) + + load_and_show(file_list, mut app) + } +} + +fn load_and_show(file_list []string, mut app App) { + app.item_list.get_items_list(file_list) + load_image(mut app) +} + +/****************************************************************************** +* +* Main +* +******************************************************************************/ +// is needed for easier diagnostics on windows +[console] +fn main() { + // mut font_path := os.resource_abs_path(os.join_path('../assets/fonts/', 'RobotoMono-Regular.ttf')) + font_name := 'RobotoMono-Regular.ttf' + font_path := os.join_path(os.temp_dir(), font_name) + println('Temporary path for the font file: [$font_path]') + + // if the font doesn't exist create it from the ebedded one + if os.exists(font_path) == false { + println('Write font [$font_name] in temp folder.') + embedded_file := $embed_file('../assets/fonts/RobotoMono-Regular.ttf') + os.write_file(font_path, embedded_file.to_string()) or { + eprintln('ERROR: not able to write font file to [$font_path]') + exit(1) + } + } + + // logo image + logo_name := 'logo.png' + logo_path := os.join_path(os.temp_dir(), logo_name) + println('Temporary path for the logo: [$logo_path]') + // if the logo doesn't exist create it from the ebedded one + if os.exists(logo_path) == false { + println('Write logo [$logo_name] in temp folder.') + embedded_file := $embed_file('../assets/logo.png') + os.write_file(logo_path, embedded_file.to_string()) or { + eprintln('ERROR: not able to write logo file to [$logo_path]') + exit(1) + } + } + + // App init + mut app := &App{ + gg: 0 + // zip fields + zip: 0 + item_list: 0 + } + + app.state = .scanning + app.logo_path = logo_path + app.font_path = font_path + + // Scan all the arguments to find images + app.item_list = &Item_list{} + // app.item_list.get_items_list(os.args[1..]) + load_and_show(os.args[1..], mut app) + + app.gg = gg.new_context( + width: win_width + height: win_height + create_window: true + window_title: 'V Image viewer 0.8' + user_data: app + bg_color: bg_color + frame_fn: frame + init_fn: app_init + cleanup_fn: cleanup + event_fn: my_event_manager + font_path: font_path + enable_dragndrop: true + max_dropped_files: 64 + max_dropped_file_path_length: 2048 + // ui_mode: true + ) + + app.gg.run() +} diff --git a/examples/viewer/zip_container.v b/examples/viewer/zip_container.v new file mode 100644 index 000000000..6e63ad095 --- /dev/null +++ b/examples/viewer/zip_container.v @@ -0,0 +1,71 @@ +/********************************************************************** +* +* Zip container manager +* +* Copyright (c) 2021 Dario Deledda. All rights reserved. +* Use of this source code is governed by an MIT license +* that can be found in the LICENSE file. +* +* TODO: +**********************************************************************/ +import szip + +fn (mut il Item_list) scan_zip(path string, in_index int) ? { + println('Scanning ZIP [$path]') + mut zp := szip.open(path, szip.CompressionLevel.no_compression, szip.OpenMode.read_only) ? + n_entries := zp.total() ? + // println(n_entries) + for index in 0 .. n_entries { + zp.open_entry_by_index(index) ? + is_dir := zp.is_dir() ? + name := zp.name() + size := zp.size() + // println("$index ${name} ${size:10} $is_dir") + + if !is_dir { + ext := get_extension(name) + if is_image(ext) == true { + il.n_item += 1 + mut item := Item{ + need_extract: true + path: path + name: name.clone() + container_index: in_index + container_item_index: index + i_type: ext + n_item: il.n_item + drawable: true + size: size + } + il.lst << item + } + } + // IMPORTANT NOTE: don't close the zip entry before we have used all the items!! + zp.close_entry() + } + zp.close() +} + +fn (mut app App) load_texture_from_zip() ?(C.sg_image, int, int) { + item := app.item_list.lst[app.item_list.item_index] + // println("Load from zip [${item.path}]") + + // open the zip + if app.zip_index != item.container_index { + if app.zip_index >= 0 { + app.zip.close() + } + app.zip_index = item.container_index + // println("Opening the zip [${item.path}]") + app.zip = szip.open(item.path, szip.CompressionLevel.no_compression, szip.OpenMode.read_only) ? + } + // println("Now get the image") + app.zip.open_entry_by_index(item.container_item_index) ? + zip_entry_size := int(item.size) + + app.resize_buf_if_needed(zip_entry_size) + + app.zip.read_entry_buf(app.mem_buf, app.mem_buf_size) ? + app.zip.close_entry() + return app.load_texture_from_buffer(app.mem_buf, zip_entry_size) +} -- 2.30.2