v/examples/viewer/view.v

833 lines
21 KiB
V

/**********************************************************************
*
* 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 sgl.Pipeline
texture gfx.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 gfx.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) gfx.Image {
sz := w * h * 4
mut img_desc := gfx.ImageDesc{
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: &u8(0)
d3d11_texture: 0
}
// comment if .dynamic is enabled
img_desc.data.subimage[0][0] = gfx.Range{
ptr: buf
size: usize(sz)
}
sg_img := gfx.make_image(&img_desc)
return sg_img
}
fn destroy_texture(sg_img gfx.Image) {
gfx.destroy_image(sg_img)
}
// Use only if: .dynamic is enabled
fn update_text_texture(sg_img gfx.Image, w int, h int, buf &byte) {
sz := w * h * 4
mut tmp_sbc := gfx.ImageData{}
tmp_sbc.subimage[0][0] = gfx.Range{
ptr: buf
size: usize(sz)
}
gfx.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) (gfx.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) (gfx.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 := gfx.PipelineDesc{}
unsafe { vmemset(&pipdesc, 0, int(sizeof(pipdesc))) }
color_state := gfx.ColorState{
blend: gfx.BlendState{
enabled: true
src_factor_rgb: .src_alpha
dst_factor_rgb: .one_minus_src_alpha
}
}
pipdesc.colors[0] = color_state
pipdesc.depth = gfx.DepthState{
write_enabled: true
compare: .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 := [u8(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 = [u8(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()
}