mirror of
https://github.com/wez/wezterm.git
synced 2024-09-11 14:25:57 +03:00
Render text on sdl using harfbuzz and freetype
This commit is contained in:
parent
c53ca64c33
commit
d82860ef5a
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/Cargo.lock
|
||||
/target/
|
||||
**/*.rs.bk
|
17
.rustfmt.toml
Normal file
17
.rustfmt.toml
Normal file
@ -0,0 +1,17 @@
|
||||
# Run `rustfmt --config-help` to see available config options.
|
||||
# Please keep these in alphabetical order.
|
||||
closure_block_indent_threshold = 1
|
||||
condense_wildcard_suffixes = true
|
||||
error_on_line_overflow = false
|
||||
error_on_line_overflow_comments = false
|
||||
# Too many derives can cause line length limits to be exceeded. rustfmt-nightly
|
||||
# 0.2.5 doesn't appear to pay attention to this.
|
||||
merge_derives = false
|
||||
reorder_imported_names = true
|
||||
reorder_imports = true
|
||||
reorder_imports_in_group = true
|
||||
# Override the Nuclide/Atom default tab spacing.
|
||||
tab_spaces = 4
|
||||
use_try_shorthand = true
|
||||
write_mode = "replace"
|
||||
max_width = 80
|
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
authors = ["Wez Furlong <wez@wezfurlong.org>"]
|
||||
name = "wterm"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
failure = "0.1.1"
|
||||
hexdump = "0.1.0"
|
||||
sdl2 = {version="0.31.0", features=["bundled"]}
|
||||
vte = "0.3.2"
|
||||
|
||||
[dependencies.font]
|
||||
path = "font"
|
10
font/Cargo.toml
Normal file
10
font/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
authors = ["Wez Furlong <wez@wezfurlong.org>"]
|
||||
name = "font"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
failure = "0.1.1"
|
||||
freetype = "0.3.0"
|
||||
harfbuzz-sys = "0.1.15"
|
||||
servo-fontconfig = "0.4.0"
|
111
font/src/ft/ftwrap.rs
Normal file
111
font/src/ft/ftwrap.rs
Normal file
@ -0,0 +1,111 @@
|
||||
//! Higher level freetype bindings
|
||||
use failure::Error;
|
||||
pub use freetype::freetype::*;
|
||||
use std::ffi::CString;
|
||||
use std::ptr;
|
||||
|
||||
/// Translate an error and value into a result
|
||||
fn ft_result<T>(err: FT_Error, t: T) -> Result<T, Error> {
|
||||
if err.succeeded() {
|
||||
Ok(t)
|
||||
} else {
|
||||
Err(format_err!("FreeType error {:?}", err))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Face {
|
||||
pub face: FT_Face,
|
||||
}
|
||||
|
||||
impl Drop for Face {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
FT_Done_Face(self.face);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Face {
|
||||
pub fn set_char_size(
|
||||
&mut self,
|
||||
char_width: FT_F26Dot6,
|
||||
char_height: FT_F26Dot6,
|
||||
horz_resolution: FT_UInt,
|
||||
vert_resolution: FT_UInt,
|
||||
) -> Result<(), Error> {
|
||||
ft_result(
|
||||
unsafe {
|
||||
FT_Set_Char_Size(
|
||||
self.face,
|
||||
char_width,
|
||||
char_height,
|
||||
horz_resolution,
|
||||
vert_resolution,
|
||||
)
|
||||
},
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn load_and_render_glyph(
|
||||
&mut self,
|
||||
glyph_index: FT_UInt,
|
||||
load_flags: FT_Int32,
|
||||
render_mode: FT_Render_Mode,
|
||||
) -> Result<&FT_GlyphSlotRec_, Error> {
|
||||
unsafe {
|
||||
let res = FT_Load_Glyph(self.face, glyph_index, load_flags);
|
||||
if res.succeeded() {
|
||||
FT_Render_Glyph((*self.face).glyph, render_mode);
|
||||
}
|
||||
ft_result(res, &*(*self.face).glyph)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Library {
|
||||
lib: FT_Library,
|
||||
}
|
||||
|
||||
impl Drop for Library {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
FT_Done_FreeType(self.lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Library {
|
||||
pub fn new() -> Result<Library, Error> {
|
||||
let mut lib = ptr::null_mut();
|
||||
let res = unsafe { FT_Init_FreeType(&mut lib as *mut _) };
|
||||
let lib = ft_result(res, lib)?;
|
||||
Ok(Library { lib })
|
||||
}
|
||||
|
||||
pub fn new_face<P>(
|
||||
&self,
|
||||
path: P,
|
||||
face_index: FT_Long,
|
||||
) -> Result<Face, Error>
|
||||
where
|
||||
P: Into<Vec<u8>>,
|
||||
{
|
||||
let mut face = ptr::null_mut();
|
||||
let path =CString::new(path.into())?;
|
||||
|
||||
let res = unsafe {
|
||||
FT_New_Face(self.lib, path.as_ptr(), face_index, &mut face as *mut _)
|
||||
};
|
||||
Ok(Face { face: ft_result(res, face)? })
|
||||
}
|
||||
|
||||
pub fn set_lcd_filter(
|
||||
&mut self,
|
||||
filter: FT_LcdFilter,
|
||||
) -> Result<(), Error> {
|
||||
unsafe {
|
||||
ft_result(FT_Library_SetLcdFilter(self.lib, filter), ())
|
||||
}
|
||||
}
|
||||
}
|
167
font/src/ft/hbwrap.rs
Normal file
167
font/src/ft/hbwrap.rs
Normal file
@ -0,0 +1,167 @@
|
||||
//! Higher level harfbuzz bindings
|
||||
use failure::Error;
|
||||
pub use harfbuzz_sys::*;
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
use std::slice;
|
||||
|
||||
pub fn language_from_string(s: &str) -> Result<hb_language_t, Error> {
|
||||
unsafe {
|
||||
let lang = hb_language_from_string(s.as_ptr() as *const i8, s.len() as i32);
|
||||
ensure!(
|
||||
!lang.is_null(),
|
||||
"failed to convert {} to language"
|
||||
);
|
||||
Ok(lang)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn feature_from_string(s: &str) -> Result<hb_feature_t, Error> {
|
||||
unsafe {
|
||||
let mut feature = mem::zeroed();
|
||||
ensure!(
|
||||
hb_feature_from_string(
|
||||
s.as_ptr() as *const i8,
|
||||
s.len() as i32,
|
||||
&mut feature as *mut _,
|
||||
) != 0,
|
||||
"failed to create feature from {}",
|
||||
s
|
||||
);
|
||||
Ok(feature)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Font {
|
||||
font: *mut hb_font_t,
|
||||
}
|
||||
|
||||
impl Drop for Font {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
hb_font_destroy(self.font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
/// Create a harfbuzz face from a freetype font
|
||||
pub fn new(face: &::ft::ftwrap::Face) -> Font {
|
||||
// hb_ft_font_create_referenced always returns a
|
||||
// pointer to something, or derefs a nullptr internally
|
||||
// if everything fails, so there's nothing for us to
|
||||
// test here.
|
||||
Font { font: unsafe { hb_ft_font_create_referenced(face.face) } }
|
||||
}
|
||||
|
||||
/// Perform shaping. On entry, Buffer holds the text to shape.
|
||||
/// Once done, Buffer holds the output glyph and position info
|
||||
pub fn shape(&mut self, buf: &mut Buffer, features: Option<&[hb_feature_t]>) {
|
||||
unsafe {
|
||||
if let Some(features) = features {
|
||||
hb_shape(self.font, buf.buf, features.as_ptr(), features.len() as u32)
|
||||
} else {
|
||||
hb_shape(self.font, buf.buf, ptr::null(), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
buf: *mut hb_buffer_t,
|
||||
}
|
||||
|
||||
impl Drop for Buffer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
hb_buffer_destroy(self.buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Create a new buffer
|
||||
pub fn new() -> Result<Buffer, Error> {
|
||||
let buf = unsafe { hb_buffer_create() };
|
||||
ensure!(
|
||||
unsafe { hb_buffer_allocation_successful(buf) } != 0,
|
||||
"hb_buffer_create failed"
|
||||
);
|
||||
Ok(Buffer { buf })
|
||||
}
|
||||
|
||||
/// Reset the buffer back to its initial post-creation state
|
||||
pub fn reset(&mut self) {
|
||||
unsafe {
|
||||
hb_buffer_reset(self.buf);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_direction(&mut self, direction: hb_direction_t) {
|
||||
unsafe {
|
||||
hb_buffer_set_direction(self.buf, direction);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_script(&mut self, script: hb_script_t) {
|
||||
unsafe {
|
||||
hb_buffer_set_script(self.buf, script);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_language(&mut self, lang: hb_language_t) {
|
||||
unsafe {
|
||||
hb_buffer_set_language(self.buf, lang);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, codepoint: hb_codepoint_t, cluster: u32) {
|
||||
unsafe {
|
||||
hb_buffer_add(self.buf, codepoint, cluster);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_utf8(&mut self, buf: &[u8]) {
|
||||
unsafe {
|
||||
hb_buffer_add_utf8(
|
||||
self.buf,
|
||||
buf.as_ptr() as *const i8,
|
||||
buf.len() as i32,
|
||||
0,
|
||||
buf.len() as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_str(&mut self, s: &str) {
|
||||
unsafe {
|
||||
hb_buffer_add_utf8(
|
||||
self.buf,
|
||||
s.as_ptr() as *const i8,
|
||||
s.len() as i32,
|
||||
0,
|
||||
s.len() as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns glyph information. This is only valid after calling
|
||||
/// font->shape() on this buffer instance.
|
||||
pub fn glyph_infos(&self) -> &[hb_glyph_info_t] {
|
||||
unsafe {
|
||||
let mut len : u32 = 0;
|
||||
let info = hb_buffer_get_glyph_infos(self.buf, &mut len as *mut _);
|
||||
slice::from_raw_parts(info, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns glyph positions. This is only valid after calling
|
||||
/// font->shape() on this buffer instance.
|
||||
pub fn glyph_positions(&self) -> &[hb_glyph_position_t] {
|
||||
unsafe {
|
||||
let mut len : u32 = 0;
|
||||
let pos = hb_buffer_get_glyph_positions(self.buf, &mut len as *mut _);
|
||||
slice::from_raw_parts(pos, len as usize)
|
||||
}
|
||||
}
|
||||
}
|
17
font/src/ft/mod.rs
Normal file
17
font/src/ft/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use failure::Error;
|
||||
pub mod ftwrap;
|
||||
pub mod hbwrap;
|
||||
use self::ftwrap::Library;
|
||||
|
||||
pub struct FTEngine {
|
||||
lib: Library,
|
||||
}
|
||||
|
||||
|
||||
impl ::FontEngine for FTEngine {
|
||||
fn new() -> Result<FTEngine, Error> {
|
||||
Ok(FTEngine {
|
||||
lib: Library::new()?,
|
||||
})
|
||||
}
|
||||
}
|
47
font/src/lib.rs
Normal file
47
font/src/lib.rs
Normal file
@ -0,0 +1,47 @@
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
|
||||
extern crate harfbuzz_sys;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
extern crate fontconfig; // from servo-fontconfig
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
extern crate freetype;
|
||||
|
||||
use failure::Error;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub mod ft;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub use ft::FTEngine as Engine;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FontDescription {
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// A user provided font description that can be used
|
||||
/// to lookup a font
|
||||
impl FontDescription {
|
||||
pub fn new<S>(name: S) -> FontDescription
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
FontDescription {
|
||||
name: name.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait FontEngine {
|
||||
fn new() -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
207
src/main.rs
Normal file
207
src/main.rs
Normal file
@ -0,0 +1,207 @@
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
extern crate hexdump;
|
||||
|
||||
use failure::Error;
|
||||
extern crate font;
|
||||
extern crate sdl2;
|
||||
use sdl2::event::{Event, WindowEvent};
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::pixels::{Color, PixelFormatEnum};
|
||||
use sdl2::rect::Rect;
|
||||
use sdl2::render::{BlendMode, Texture, TextureCreator};
|
||||
|
||||
use font::ft::ftwrap;
|
||||
use font::ft::hbwrap;
|
||||
use std::mem;
|
||||
use std::slice;
|
||||
|
||||
struct Glyph<'a> {
|
||||
tex: Texture<'a>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
x_advance: i32,
|
||||
y_advance: i32,
|
||||
x_offset: i32,
|
||||
y_offset: i32,
|
||||
bearing_x: i32,
|
||||
bearing_y: i32,
|
||||
}
|
||||
|
||||
impl<'a> Glyph<'a> {
|
||||
fn new<T>(
|
||||
texture_creator: &'a TextureCreator<T>,
|
||||
glyph: &ftwrap::FT_GlyphSlotRec_,
|
||||
pos: &hbwrap::hb_glyph_position_t,
|
||||
) -> Result<Glyph<'a>, Error> {
|
||||
let mode: ftwrap::FT_Pixel_Mode =
|
||||
unsafe { mem::transmute(glyph.bitmap.pixel_mode as u32) };
|
||||
|
||||
// pitch is the number of bytes per source row
|
||||
let pitch = glyph.bitmap.pitch.abs() as usize;
|
||||
|
||||
match mode {
|
||||
ftwrap::FT_Pixel_Mode::FT_PIXEL_MODE_LCD => {
|
||||
let width = (glyph.bitmap.width as usize) / 3;
|
||||
let height = glyph.bitmap.rows as usize;
|
||||
let mut tex = texture_creator
|
||||
.create_texture_static(
|
||||
Some(PixelFormatEnum::BGR24),
|
||||
width as u32,
|
||||
height as u32,
|
||||
)
|
||||
.map_err(failure::err_msg)?;
|
||||
let data = unsafe {
|
||||
slice::from_raw_parts(glyph.bitmap.buffer, height * pitch)
|
||||
};
|
||||
tex.update(None, data, pitch)?;
|
||||
|
||||
Ok(Glyph {
|
||||
tex,
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
x_advance: pos.x_advance / 64,
|
||||
y_advance: pos.y_advance / 64,
|
||||
x_offset: pos.x_offset / 64,
|
||||
y_offset: pos.y_offset / 64,
|
||||
bearing_x: glyph.bitmap_left,
|
||||
bearing_y: glyph.bitmap_top,
|
||||
})
|
||||
}
|
||||
mode @ _ => bail!("unhandled pixel mode: {:?}", mode),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn glyphs_for_text<'a, T>(
|
||||
texture_creator: &'a TextureCreator<T>,
|
||||
s: &str,
|
||||
) -> Result<Vec<Glyph<'a>>, Error> {
|
||||
let mut lib = ftwrap::Library::new()?;
|
||||
lib.set_lcd_filter(
|
||||
ftwrap::FT_LcdFilter::FT_LCD_FILTER_DEFAULT,
|
||||
)?;
|
||||
let mut face =
|
||||
lib.new_face("/home/wez/.fonts/OperatorMonoLig-Book.otf", 0)?;
|
||||
face.set_char_size(0, 36 * 64, 96, 96)?;
|
||||
let mut font = hbwrap::Font::new(&face);
|
||||
let lang = hbwrap::language_from_string("en")?;
|
||||
let mut buf = hbwrap::Buffer::new()?;
|
||||
buf.set_script(hbwrap::HB_SCRIPT_LATIN);
|
||||
buf.set_direction(hbwrap::HB_DIRECTION_LTR);
|
||||
buf.set_language(lang);
|
||||
buf.add_str(s);
|
||||
let features = vec![
|
||||
// kerning
|
||||
hbwrap::feature_from_string("kern")?,
|
||||
// ligatures
|
||||
hbwrap::feature_from_string("liga")?,
|
||||
// contextual ligatures
|
||||
hbwrap::feature_from_string("clig")?,
|
||||
];
|
||||
font.shape(&mut buf, Some(features.as_slice()));
|
||||
|
||||
let infos = buf.glyph_infos();
|
||||
let positions = buf.glyph_positions();
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (i, info) in infos.iter().enumerate() {
|
||||
let pos = &positions[i];
|
||||
println!(
|
||||
"info {} glyph_pos={}, cluster={} x_adv={} y_adv={} x_off={} y_off={}",
|
||||
i,
|
||||
info.codepoint,
|
||||
info.cluster,
|
||||
pos.x_advance,
|
||||
pos.y_advance,
|
||||
pos.x_offset,
|
||||
pos.y_offset
|
||||
);
|
||||
|
||||
let glyph = face.load_and_render_glyph(
|
||||
info.codepoint,
|
||||
(ftwrap::FT_LOAD_COLOR) as i32,
|
||||
ftwrap::FT_Render_Mode::FT_RENDER_MODE_LCD,
|
||||
)?;
|
||||
|
||||
|
||||
let g = Glyph::new(texture_creator, glyph, pos)?;
|
||||
|
||||
/*
|
||||
println!(
|
||||
"width={} height={} advx={} advy={} bearing={},{}",
|
||||
g.width,
|
||||
g.height,
|
||||
g.x_advance,
|
||||
g.y_advance,
|
||||
g.bearing_x,
|
||||
g.bearing_y
|
||||
); */
|
||||
|
||||
result.push(g);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Error> {
|
||||
|
||||
let sdl_context = sdl2::init().map_err(failure::err_msg)?;
|
||||
let video_subsys = sdl_context.video().map_err(failure::err_msg)?;
|
||||
let window = video_subsys
|
||||
.window("wterm", 1024, 768)
|
||||
.resizable()
|
||||
.opengl()
|
||||
.build()?;
|
||||
let mut canvas = window.into_canvas().build()?;
|
||||
let texture_creator = canvas.texture_creator();
|
||||
let glyphs = glyphs_for_text(&texture_creator, "foo->bar();")?;
|
||||
|
||||
for event in sdl_context
|
||||
.event_pump()
|
||||
.map_err(failure::err_msg)?
|
||||
.wait_iter()
|
||||
{
|
||||
match event {
|
||||
Event::KeyDown { keycode: Some(Keycode::Escape), .. } |
|
||||
Event::Quit { .. } => break,
|
||||
Event::Window { win_event: WindowEvent::Resized(..), .. } => {
|
||||
println!("resize");
|
||||
}
|
||||
Event::Window { win_event: WindowEvent::Exposed, .. } => {
|
||||
println!("exposed");
|
||||
canvas.set_draw_color(Color::RGBA(0, 0, 0, 255));
|
||||
canvas.clear();
|
||||
canvas.set_blend_mode(BlendMode::Blend);
|
||||
|
||||
let mut x = 10i32;
|
||||
let mut y = 100i32;
|
||||
for g in glyphs.iter() {
|
||||
canvas
|
||||
.copy(
|
||||
&g.tex,
|
||||
Some(Rect::new(0, 0, g.width, g.height)),
|
||||
Some(Rect::new(
|
||||
x + g.x_offset - g.bearing_x,
|
||||
y - (g.y_offset + g.bearing_y as i32) as i32,
|
||||
g.width,
|
||||
g.height,
|
||||
)),
|
||||
)
|
||||
.map_err(failure::err_msg)?;
|
||||
x += g.x_advance;
|
||||
y += g.y_advance;
|
||||
}
|
||||
|
||||
canvas.present();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
run().unwrap();
|
||||
}
|
Loading…
Reference in New Issue
Block a user