/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use atomic_float::AtomicF32;
use miniquad::{TextureId, UniformType};
use std::{
collections::HashMap,
io::{BufRead, BufReader},
path::Path,
sync::{
atomic::Ordering,
Arc, Mutex,
},
};
use crate::{
error::Result,
gfx::{
FreetypeFace, Point, Rectangle, RenderContext,
COLOR_RED, COLOR_WHITE,
},
prop::PropertyBool,
scene::{Pimpl, SceneGraph, SceneNodeId},
text::{Glyph, TextShaper},
};
fn read_lines
(filename: P) -> Vec
where
P: AsRef,
{
//let file = File::open(filename).unwrap();
//BufReader::new(file).lines().map(|l| l.unwrap()).collect()
// Just so we can package for android easily
// Later this will be all replaced anyway
let file = include_bytes!("../chat.txt");
BufReader::new(&file[..]).lines().map(|l| l.unwrap()).collect()
}
pub type ChatViewPtr = Arc;
pub struct ChatView {
node_name: String,
debug: PropertyBool,
// Used for mouse interaction
world_rect: Mutex>,
mouse_pos: Mutex>,
text_shaper: TextShaper,
scroll: AtomicF32,
lines: Vec,
glyph_lines: Mutex>>,
atlas: Mutex>,
}
impl ChatView {
pub fn new(
scene_graph: &mut SceneGraph,
node_id: SceneNodeId,
font_faces: Vec,
) -> Result {
let node = scene_graph.get_node(node_id).unwrap();
let node_name = node.name.clone();
let debug = PropertyBool::wrap(node, "debug", 0)?;
let text_shaper = TextShaper { font_faces };
let lines = read_lines("chat.txt");
let mut glyph_lines = vec![];
glyph_lines.resize(lines.len(), vec![]);
let self_ = Arc::new(Self {
node_name: node_name.clone(),
debug,
world_rect: Mutex::new(Rectangle { x: 0., y: 0., w: 0., h: 0. }),
mouse_pos: Mutex::new(Point { x: 0., y: 0. }),
text_shaper,
scroll: AtomicF32::new(0.),
lines,
glyph_lines: Mutex::new(glyph_lines),
atlas: Mutex::new(HashMap::new()),
});
/*
let weak_self = Arc::downgrade(&self_);
let slot_move = Slot {
name: format!("{}::mouse_move", node_name),
func: Box::new(move |data| {
let mut cur = Cursor::new(&data);
let x = f32::decode(&mut cur).unwrap();
let y = f32::decode(&mut cur).unwrap();
let self_ = weak_self.upgrade();
if let Some(self_) = self_ {
let pos = &mut *self_.mouse_pos.lock().unwrap();
pos.x = x;
pos.y = y;
}
}),
};
let weak_self = Arc::downgrade(&self_);
let slot_wheel = Slot {
name: format!("{}::mouse_wheel", node_name),
func: Box::new(move |data| {
let mut cur = Cursor::new(&data);
let x = f32::decode(&mut cur).unwrap();
let y = f32::decode(&mut cur).unwrap();
let self_ = weak_self.upgrade();
if let Some(self_) = self_ {
self_.mouse_scroll(x, y);
}
}),
};
*/
let mouse_node =
scene_graph.lookup_node_mut("/window/input/mouse").expect("no mouse attached!");
//mouse_node.register("wheel", slot_wheel);
//mouse_node.register("move", slot_move);
// Save any properties we use
Ok(Pimpl::ChatView(self_))
}
pub fn render<'a>(
&self,
render: &mut RenderContext<'a>,
node_id: SceneNodeId,
layer_rect: &Rectangle,
) -> Result<()> {
let debug = self.debug.get();
let node = render.scene_graph.get_node(node_id).unwrap();
let rect = RenderContext::get_dim(node, layer_rect)?;
// Used for detecting mouse clicks
let mut world_rect = rect.clone();
world_rect.x += layer_rect.x as f32;
world_rect.y += layer_rect.y as f32;
*self.world_rect.lock().unwrap() = world_rect;
let layer_w = layer_rect.w as f32;
let layer_h = layer_rect.h as f32;
let off_x = rect.x / layer_w;
let off_y = rect.y / layer_h;
// Use absolute pixel scale
let scale_x = 1. / layer_w;
let scale_y = 1. / layer_h;
let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) *
glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.));
let mut uniforms_data = [0u8; 128];
let data: [u8; 64] = unsafe { std::mem::transmute_copy(&render.proj) };
uniforms_data[0..64].copy_from_slice(&data);
let data: [u8; 64] = unsafe { std::mem::transmute_copy(&model) };
uniforms_data[64..].copy_from_slice(&data);
assert_eq!(128, 2 * UniformType::Mat4.size());
render.ctx.apply_uniforms_from_bytes(uniforms_data.as_ptr(), uniforms_data.len());
// Used for scaling the font size
let window = render.scene_graph.lookup_node("/window").expect("no window attached!");
let window_scale = window.get_property_f32("scale")?;
let font_size = window_scale * 20.;
let bound = Rectangle { x: 0., y: 0., w: rect.w, h: rect.h };
let glyph_lines = &mut self.glyph_lines.lock().unwrap();
let atlas = &mut self.atlas.lock().unwrap();
let scroll = self.scroll.load(Ordering::Relaxed);
for (i, (line, glyph_line)) in self.lines.iter().zip(glyph_lines.iter_mut()).enumerate() {
let line = line.replace("\t", " ");
// Split time and nick from the line
let mut iter = line.split_whitespace();
let Some(time) = iter.next() else {
error!("line missing time");
continue
};
let Some(nick) = iter.next() else {
error!("line missing nick");
continue
};
let Some(line) = iter.remainder() else {
error!("line missing remainder");
continue
};
let linespacing = window_scale * 30.;
let off_y = linespacing * i as f32 + scroll;
if off_y + linespacing < 0. || off_y - linespacing > rect.h {
continue;
}
if glyph_line.is_empty() {
*glyph_line = self.text_shaper.shape(line.to_string(), font_size, COLOR_WHITE);
}
let times_color = [0.4, 0.4, 0.4, 1.];
let times_color_u8 =
[(255. * 0.4) as u8, (255. * 0.4) as u8, (255. * 0.4) as u8, (255. * 1.) as u8];
let glyphs_time = self.text_shaper.shape(time.to_string(), font_size, times_color);
let mut rhs = 0.;
for glyph in glyphs_time {
let mut pos = glyph.pos.clone();
pos.y += off_y;
rhs = pos.x + pos.w;
assert_eq!(glyph.bmp.len() as u16, glyph.bmp_width * glyph.bmp_height * 4);
//debug!("gly {} {}", glyph.substr, glyph.bmp.len());
let texture = if atlas.contains_key(&(glyph.id, times_color_u8.clone())) {
*atlas.get(&(glyph.id, times_color_u8.clone())).unwrap()
} else {
let texture = render.ctx.new_texture_from_rgba8(
glyph.bmp_width,
glyph.bmp_height,
&glyph.bmp,
);
atlas.insert((glyph.id, times_color_u8.clone()), texture);
texture
};
render.render_clipped_box_with_texture2(&bound, &pos, COLOR_WHITE, texture);
//render.ctx.delete_texture(texture);
}
let nick_colors = [
[0.00, 0.94, 1.00, 1.],
[0.36, 1.00, 0.69, 1.],
[0.29, 1.00, 0.45, 1.],
[0.00, 0.73, 0.38, 1.],
[0.21, 0.67, 0.67, 1.],
[0.56, 0.61, 1.00, 1.],
[0.84, 0.48, 1.00, 1.],
[1.00, 0.61, 0.94, 1.],
[1.00, 0.36, 0.48, 1.],
[1.00, 0.30, 0.00, 1.],
];
let nick_colors_u8 = [
[(255. * 0.00) as u8, (255. * 0.94) as u8, (255. * 1.00) as u8, (255. * 1.) as u8],
[(255. * 0.36) as u8, (255. * 1.00) as u8, (255. * 0.69) as u8, (255. * 1.) as u8],
[(255. * 0.29) as u8, (255. * 1.00) as u8, (255. * 0.45) as u8, (255. * 1.) as u8],
[(255. * 0.00) as u8, (255. * 0.73) as u8, (255. * 0.38) as u8, (255. * 1.) as u8],
[(255. * 0.21) as u8, (255. * 0.67) as u8, (255. * 0.67) as u8, (255. * 1.) as u8],
[(255. * 0.56) as u8, (255. * 0.61) as u8, (255. * 1.00) as u8, (255. * 1.) as u8],
[(255. * 0.84) as u8, (255. * 0.48) as u8, (255. * 1.00) as u8, (255. * 1.) as u8],
[(255. * 1.00) as u8, (255. * 0.61) as u8, (255. * 0.94) as u8, (255. * 1.) as u8],
[(255. * 1.00) as u8, (255. * 0.36) as u8, (255. * 0.48) as u8, (255. * 1.) as u8],
[(255. * 1.00) as u8, (255. * 0.30) as u8, (255. * 0.00) as u8, (255. * 1.) as u8],
];
let nick_color = nick_colors[nick.len() % nick_colors.len()];
let nick_color_u8 = nick_colors_u8[nick.len() % nick_colors.len()];
let glyphs_nick = self.text_shaper.shape(nick.to_string(), font_size, nick_color);
let off_x = rhs + window_scale * 20.;
for glyph in glyphs_nick {
let mut pos = glyph.pos.clone();
pos.x += off_x;
pos.y += off_y;
rhs = pos.x + pos.w;
assert_eq!(glyph.bmp.len() as u16, glyph.bmp_width * glyph.bmp_height * 4);
//debug!("gly {} {}", glyph.substr, glyph.bmp.len());
//let texture = render.ctx.new_texture_from_rgba8(glyph.bmp_width, glyph.bmp_height, &glyph.bmp);
let texture = if atlas.contains_key(&(glyph.id, nick_color_u8.clone())) {
*atlas.get(&(glyph.id, nick_color_u8.clone())).unwrap()
} else {
let texture = render.ctx.new_texture_from_rgba8(
glyph.bmp_width,
glyph.bmp_height,
&glyph.bmp,
);
atlas.insert((glyph.id, nick_color_u8.clone()), texture);
texture
};
render.render_clipped_box_with_texture2(&bound, &pos, COLOR_WHITE, texture);
//render.ctx.delete_texture(texture);
}
let off_x = rhs + window_scale * 20.;
for glyph in glyph_line {
let mut pos = glyph.pos.clone();
pos.x += off_x;
pos.y += off_y;
assert_eq!(glyph.bmp.len() as u16, glyph.bmp_width * glyph.bmp_height * 4);
//debug!("gly {} {}", glyph.substr, glyph.bmp.len());
//let texture = render.ctx.new_texture_from_rgba8(glyph.bmp_width, glyph.bmp_height, &glyph.bmp);
let texture = if atlas.contains_key(&(glyph.id, [255, 255, 255, 255])) {
*atlas.get(&(glyph.id, [255, 255, 255, 255])).unwrap()
} else {
let texture = render.ctx.new_texture_from_rgba8(
glyph.bmp_width,
glyph.bmp_height,
&glyph.bmp,
);
atlas.insert((glyph.id, [255, 255, 255, 255]), texture);
texture
};
render.render_clipped_box_with_texture2(&bound, &pos, COLOR_WHITE, texture);
//render.ctx.delete_texture(texture);
}
}
if debug {
render.outline(0., 0., rect.w, rect.h, COLOR_RED, 1.);
}
Ok(())
}
fn mouse_scroll(self: Arc, _x: f32, y: f32) {
let mouse_pos = &*self.mouse_pos.lock().unwrap();
if !self.world_rect.lock().unwrap().contains(mouse_pos) {
return;
}
drop(mouse_pos);
self.scroll.fetch_add(y * 10., Ordering::Relaxed);
// y = 1 for scroll up
// y = -1 for scroll down
//println!("{}", y);
}
}