slint/sixtyfps_runtime/rendering_backends/gl/lib.rs
2021-02-10 12:49:26 +01:00

1413 lines
51 KiB
Rust

/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
SPDX-License-Identifier: GPL-3.0-only
This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information.
LICENSE END */
use std::cell::RefCell;
use std::collections::HashMap;
use std::pin::Pin;
use std::rc::{Rc, Weak};
use sixtyfps_corelib::graphics::{
Brush, Color, FontMetrics, FontRequest, Point, Rect, RenderingCache, Resource, Size,
};
use sixtyfps_corelib::item_rendering::{CachedRenderingData, ItemRenderer};
use sixtyfps_corelib::items::{
ImageFit, Item, TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap,
};
use sixtyfps_corelib::properties::Property;
use sixtyfps_corelib::window::ComponentWindow;
use sixtyfps_corelib::SharedString;
mod graphics_window;
use graphics_window::*;
pub(crate) mod eventloop;
mod images;
mod svg;
use images::*;
type CanvasRc = Rc<RefCell<femtovg::Canvas<femtovg::renderer::OpenGl>>>;
pub const DEFAULT_FONT_SIZE: f32 = 12.;
pub const DEFAULT_FONT_WEIGHT: i32 = 400; // CSS normal
#[derive(PartialEq, Eq, Hash, Debug)]
enum ImageCacheKey {
Path(String),
EmbeddedData(by_address::ByAddress<&'static [u8]>),
}
impl ImageCacheKey {
fn new(resource: &Resource) -> Option<Self> {
Some(match resource {
Resource::None => return None,
Resource::AbsoluteFilePath(path) => {
if path.is_empty() {
return None;
}
Self::Path(path.to_string())
}
Resource::EmbeddedData(data) => {
Self::EmbeddedData(by_address::ByAddress(data.as_slice()))
}
Resource::EmbeddedRgbaImage { .. } => return None,
})
}
}
#[derive(Clone)]
enum ItemGraphicsCacheEntry {
Image(Rc<CachedImage>),
ColorizedImage {
// This original image Rc is kept here to keep the image in the shared image cache, so that
// changes to the colorization brush will not require re-uploading the image.
original_image: Rc<CachedImage>,
colorized_image: Rc<CachedImage>,
},
}
impl ItemGraphicsCacheEntry {
fn as_image(&self) -> &Rc<CachedImage> {
match self {
ItemGraphicsCacheEntry::Image(image) => image,
ItemGraphicsCacheEntry::ColorizedImage { colorized_image, .. } => colorized_image,
//_ => panic!("internal error. image requested for non-image gpu data"),
}
}
fn is_colorized_image(&self) -> bool {
matches!(self, ItemGraphicsCacheEntry::ColorizedImage{..})
}
}
struct FontCache(HashMap<FontCacheKey, femtovg::FontId>);
impl Default for FontCache {
fn default() -> Self {
Self(HashMap::new())
}
}
mod fonts;
pub use fonts::register_application_font_from_memory;
use fonts::*;
impl FontCache {
fn load_single_font(&mut self, canvas: &CanvasRc, request: &FontRequest) -> femtovg::FontId {
self.0
.entry(FontCacheKey { family: request.family.clone(), weight: request.weight.unwrap() })
.or_insert_with(|| {
try_load_app_font(canvas, &request)
.unwrap_or_else(|| load_system_font(canvas, &request))
})
.clone()
}
fn font(&mut self, canvas: &CanvasRc, mut request: FontRequest, scale_factor: f32) -> GLFont {
request.pixel_size = request.pixel_size.or(Some(DEFAULT_FONT_SIZE * scale_factor));
request.weight = request.weight.or(Some(DEFAULT_FONT_WEIGHT));
let primary_font = self.load_single_font(canvas, &request);
let fallbacks = font_fallbacks_for_request(&request);
let fonts = core::iter::once(primary_font)
.chain(
fallbacks
.iter()
.map(|fallback_request| self.load_single_font(canvas, &fallback_request)),
)
.collect::<Vec<_>>();
GLFont { fonts, canvas: canvas.clone(), pixel_size: request.pixel_size.unwrap() }
}
}
// glutin's WindowedContext tries to enforce being current or not. Since we need the WindowedContext's window() function
// in the GL renderer regardless whether we're current or not, we wrap the two states back into one type.
#[cfg(not(target_arch = "wasm32"))]
enum WindowedContextWrapper {
NotCurrent(glutin::WindowedContext<glutin::NotCurrent>),
Current(glutin::WindowedContext<glutin::PossiblyCurrent>),
}
#[cfg(not(target_arch = "wasm32"))]
impl WindowedContextWrapper {
fn window(&self) -> &winit::window::Window {
match self {
Self::NotCurrent(context) => context.window(),
Self::Current(context) => context.window(),
}
}
fn make_current(self) -> Self {
match self {
Self::NotCurrent(not_current_ctx) => {
let current_ctx = unsafe { not_current_ctx.make_current().unwrap() };
Self::Current(current_ctx)
}
this @ Self::Current(_) => this,
}
}
fn make_not_current(self) -> Self {
match self {
this @ Self::NotCurrent(_) => this,
Self::Current(current_ctx_rc) => {
Self::NotCurrent(unsafe { current_ctx_rc.make_not_current().unwrap() })
}
}
}
fn swap_buffers(&mut self) {
match self {
WindowedContextWrapper::NotCurrent(_) => {}
WindowedContextWrapper::Current(current_ctx) => {
current_ctx.swap_buffers().unwrap();
}
}
}
}
struct GLRendererData {
canvas: CanvasRc,
#[cfg(target_arch = "wasm32")]
window: Rc<winit::window::Window>,
#[cfg(not(target_arch = "wasm32"))]
windowed_context: RefCell<Option<WindowedContextWrapper>>,
#[cfg(target_arch = "wasm32")]
event_loop_proxy: Rc<winit::event_loop::EventLoopProxy<eventloop::CustomEvent>>,
item_graphics_cache: RefCell<RenderingCache<Option<ItemGraphicsCacheEntry>>>,
// Cache used to avoid repeatedly decoding images from disk. The weak references are
// drained after flushing the renderer commands to the screen.
image_cache: RefCell<HashMap<ImageCacheKey, Weak<CachedImage>>>,
loaded_fonts: RefCell<FontCache>,
}
impl GLRendererData {
// Look up the given image cache key in the image cache and upgrade the weak reference to a strong one if found,
// otherwise a new image is created/loaded from the given callback.
fn lookup_image_in_cache_or_create(
&self,
cache_key: ImageCacheKey,
image_create_fn: impl Fn() -> Option<Rc<CachedImage>>,
) -> Option<Rc<CachedImage>> {
match self.image_cache.borrow_mut().entry(cache_key) {
std::collections::hash_map::Entry::Occupied(mut existing_entry) => {
existing_entry.get().upgrade().or_else(|| {
let new_image = image_create_fn()?;
existing_entry.insert(Rc::downgrade(&new_image));
Some(new_image)
})
}
std::collections::hash_map::Entry::Vacant(vacant_entry) => {
let new_image = image_create_fn()?;
vacant_entry.insert(Rc::downgrade(&new_image));
Some(new_image)
}
}
}
// Try to load the image the given resource points to
fn load_image_resource(&self, resource: Resource) -> Option<ItemGraphicsCacheEntry> {
let cache_key = ImageCacheKey::new(&resource)?;
let cached_image = self.lookup_image_in_cache_or_create(cache_key, || {
CachedImage::new_from_resource(&resource, self)
})?;
Some(ItemGraphicsCacheEntry::Image(cached_image))
}
// Load the image from the specified load factory fn, unless it was cached in the item's rendering
// cache.
fn load_cached_item_image_from_function(
&self,
item_cache: &CachedRenderingData,
load_fn: impl FnOnce() -> Option<ItemGraphicsCacheEntry>,
) -> Option<ItemGraphicsCacheEntry> {
let mut cache = self.item_graphics_cache.borrow_mut();
item_cache.ensure_up_to_date(&mut cache, || load_fn())
}
}
pub struct GLRenderer {
shared_data: Rc<GLRendererData>,
}
impl GLRenderer {
pub(crate) fn new(
event_loop: &dyn crate::eventloop::EventLoopInterface,
window_builder: winit::window::WindowBuilder,
#[cfg(target_arch = "wasm32")] canvas_id: &str,
) -> GLRenderer {
#[cfg(not(target_arch = "wasm32"))]
let (windowed_context, renderer) = {
let windowed_context = glutin::ContextBuilder::new()
.with_vsync(true)
.build_windowed(window_builder, event_loop.event_loop_target())
.unwrap();
let windowed_context = unsafe { windowed_context.make_current().unwrap() };
let renderer = femtovg::renderer::OpenGl::new(|symbol| {
windowed_context.get_proc_address(symbol) as *const _
})
.unwrap();
#[cfg(target_os = "macos")]
{
use cocoa::appkit::NSView;
use winit::platform::macos::WindowExtMacOS;
let ns_view = windowed_context.window().ns_view();
let view_id: cocoa::base::id = ns_view as *const _ as *mut _;
unsafe {
NSView::setLayerContentsPlacement(view_id, cocoa::appkit::NSViewLayerContentsPlacement::NSViewLayerContentsPlacementTopLeft)
}
}
(windowed_context, renderer)
};
#[cfg(target_arch = "wasm32")]
let event_loop_proxy = Rc::new(event_loop.event_loop_proxy().clone());
#[cfg(target_arch = "wasm32")]
let (window, renderer) = {
use wasm_bindgen::JsCast;
let canvas = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id(canvas_id)
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
use winit::platform::web::WindowBuilderExtWebSys;
use winit::platform::web::WindowExtWebSys;
let existing_canvas_size = winit::dpi::LogicalSize::new(
canvas.client_width() as u32,
canvas.client_height() as u32,
);
let window = Rc::new(
window_builder
.with_canvas(Some(canvas.clone()))
.build(&event_loop.event_loop_target())
.unwrap(),
);
// Try to maintain the existing size of the canvas element. A window created with winit
// on the web will always have 1024x768 as size otherwise.
let resize_canvas = {
let event_loop_proxy = event_loop_proxy.clone();
let window = window.clone();
let canvas = canvas.clone();
move |_: web_sys::Event| {
let existing_canvas_size = winit::dpi::LogicalSize::new(
canvas.client_width() as u32,
canvas.client_height() as u32,
);
window.set_inner_size(existing_canvas_size);
window.request_redraw();
event_loop_proxy.send_event(eventloop::CustomEvent::WakeUpAndPoll).ok();
}
};
let resize_closure =
wasm_bindgen::closure::Closure::wrap(Box::new(resize_canvas) as Box<dyn FnMut(_)>);
web_sys::window()
.unwrap()
.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())
.unwrap();
resize_closure.forget();
{
let default_size = window.inner_size().to_logical(window.scale_factor());
let new_size = winit::dpi::LogicalSize::new(
if existing_canvas_size.width > 0 {
existing_canvas_size.width
} else {
default_size.width
},
if existing_canvas_size.height > 0 {
existing_canvas_size.height
} else {
default_size.height
},
);
if new_size != default_size {
window.set_inner_size(new_size);
}
}
let renderer =
femtovg::renderer::OpenGl::new_from_html_canvas(&window.canvas()).unwrap();
(window, renderer)
};
let canvas = femtovg::Canvas::new(renderer).unwrap();
let shared_data = GLRendererData {
canvas: Rc::new(RefCell::new(canvas)),
#[cfg(not(target_arch = "wasm32"))]
windowed_context: RefCell::new(Some(WindowedContextWrapper::NotCurrent(unsafe {
windowed_context.make_not_current().unwrap()
}))),
#[cfg(target_arch = "wasm32")]
window,
#[cfg(target_arch = "wasm32")]
event_loop_proxy,
item_graphics_cache: Default::default(),
image_cache: Default::default(),
loaded_fonts: Default::default(),
};
GLRenderer { shared_data: Rc::new(shared_data) }
}
/// Returns a new item renderer instance. At this point rendering begins and the backend ensures that the
/// window background was cleared with the specified clear_color.
fn new_renderer(&mut self, clear_color: &Color, scale_factor: f32) -> GLItemRenderer {
let size = self.window().inner_size();
#[cfg(not(target_arch = "wasm32"))]
{
let ctx = &mut *self.shared_data.windowed_context.borrow_mut();
*ctx = ctx.take().unwrap().make_current().into();
}
{
let mut canvas = self.shared_data.canvas.borrow_mut();
// We pass 1.0 as dpi / device pixel ratio as femtovg only uses this factor to scale
// text metrics. Since we do the entire translation from logical pixels to physical
// pixels on our end, we don't need femtovg to scale a second time.
canvas.set_size(size.width, size.height, 1.0);
canvas.clear_rect(0, 0, size.width, size.height, clear_color.into());
}
GLItemRenderer {
shared_data: self.shared_data.clone(),
scale_factor,
saved_scissor_rects: Vec::new(),
current_scissor_rect: None,
}
}
/// Complete the item rendering by calling this function. This will typically flush any remaining/pending
/// commands to the underlying graphics subsystem.
fn flush_renderer(&mut self, _renderer: GLItemRenderer) {
self.shared_data.canvas.borrow_mut().flush();
#[cfg(not(target_arch = "wasm32"))]
{
let mut ctx = self.shared_data.windowed_context.borrow_mut().take().unwrap();
ctx.swap_buffers();
*self.shared_data.windowed_context.borrow_mut() = ctx.make_not_current().into();
}
self.shared_data.image_cache.borrow_mut().retain(|_, cached_image_weak| {
cached_image_weak
.upgrade()
.map_or(false, |cached_image_rc| Rc::strong_count(&cached_image_rc) > 1)
});
}
#[cfg(not(target_arch = "wasm32"))]
fn window(&self) -> std::cell::Ref<winit::window::Window> {
std::cell::Ref::map(self.shared_data.windowed_context.borrow(), |ctx| {
ctx.as_ref().unwrap().window()
})
}
#[cfg(target_arch = "wasm32")]
fn window(&self) -> &winit::window::Window {
return &self.shared_data.window;
}
/// Returns a FontMetrics trait object that can be used to measure text and that matches the given font request as
/// closely as possible.
fn font_metrics(&mut self, request: FontRequest, scale_factor: f32) -> Box<dyn FontMetrics> {
Box::new(GLFontMetrics { request, scale_factor, shared_data: self.shared_data.clone() })
}
/// Returns the size of image referenced by the specified resource. These are image pixels, not adjusted
/// to the window scale factor.
fn image_size(
&self,
item_graphics_cache: &sixtyfps_corelib::item_rendering::CachedRenderingData,
source: core::pin::Pin<&sixtyfps_corelib::properties::Property<Resource>>,
) -> sixtyfps_corelib::graphics::Size {
self.shared_data
.load_cached_item_image_from_function(item_graphics_cache, || {
self.shared_data.load_image_resource(source.get())
})
.and_then(|image| image.as_image().size())
.unwrap_or_else(|| Size::new(1., 1.))
}
}
pub struct GLItemRenderer {
shared_data: Rc<GLRendererData>,
scale_factor: f32,
saved_scissor_rects: Vec<Rect>,
current_scissor_rect: Option<Rect>,
}
fn rect_to_path(r: Rect) -> femtovg::Path {
let mut path = femtovg::Path::new();
path.rect(r.min_x(), r.min_y(), r.width(), r.height());
path
}
impl ItemRenderer for GLItemRenderer {
fn draw_rectangle(
&mut self,
pos: Point,
rect: std::pin::Pin<&sixtyfps_corelib::items::Rectangle>,
) {
let geometry = rect.geometry();
if geometry.is_empty() {
return;
}
// TODO: cache path in item to avoid re-tesselation
let mut path = rect_to_path(geometry);
let paint = match self.brush_to_paint(rect.background(), &mut path) {
Some(paint) => paint,
None => return,
};
self.shared_data.canvas.borrow_mut().save_with(|canvas| {
canvas.translate(pos.x, pos.y);
canvas.fill_path(&mut path, paint)
})
}
fn draw_border_rectangle(
&mut self,
pos: Point,
rect: std::pin::Pin<&sixtyfps_corelib::items::BorderRectangle>,
) {
let geometry = rect.geometry();
if geometry.is_empty() {
return;
}
// If the border width exceeds the width, just fill the rectangle.
let border_width = rect.border_width().min(rect.width() / 2.);
// In CSS the border is entirely towards the inside of the boundary
// geometry, while in femtovg the line with for a stroke is 50% in-
// and 50% outwards. We choose the CSS model, so the inner rectangle
// is adjusted accordingly.
let mut path = femtovg::Path::new();
let border_radius = rect.border_radius();
let x = geometry.min_x() + border_width / 2.;
let y = geometry.min_y() + border_width / 2.;
let width = geometry.width() - border_width;
let height = geometry.height() - border_width;
// If we're drawing a circle, use directly connected bezier curves instead of
// ones with intermediate LineTo verbs, as `rounded_rect` creates, to avoid
// rendering artifacts due to those edges.
if width == height && border_radius * 2. == width {
path.circle(x + border_radius, y + border_radius, border_radius);
} else {
path.rounded_rect(x, y, width, height, border_radius);
}
let fill_paint = self.brush_to_paint(rect.background(), &mut path);
let border_paint = self.brush_to_paint(rect.border_color(), &mut path).map(|mut paint| {
paint.set_line_width(border_width);
paint
});
self.shared_data.canvas.borrow_mut().save_with(|canvas| {
canvas.translate(pos.x, pos.y);
fill_paint.map(|paint| canvas.fill_path(&mut path, paint));
border_paint.map(|border_paint| canvas.stroke_path(&mut path, border_paint));
})
}
fn draw_image(&mut self, pos: Point, image: std::pin::Pin<&sixtyfps_corelib::items::Image>) {
self.draw_image_impl(
pos + euclid::Vector2D::new(image.x(), image.y()),
&image.cached_rendering_data,
sixtyfps_corelib::items::Image::FIELD_OFFSETS.source.apply_pin(image),
Rect::default(),
image.width(),
image.height(),
image.image_fit(),
None,
);
}
fn draw_clipped_image(
&mut self,
pos: Point,
clipped_image: std::pin::Pin<&sixtyfps_corelib::items::ClippedImage>,
) {
let source_clip_rect = Rect::new(
[clipped_image.source_clip_x() as _, clipped_image.source_clip_y() as _].into(),
[clipped_image.source_clip_width() as _, clipped_image.source_clip_height() as _]
.into(),
);
self.draw_image_impl(
pos + euclid::Vector2D::new(clipped_image.x(), clipped_image.y()),
&clipped_image.cached_rendering_data,
sixtyfps_corelib::items::ClippedImage::FIELD_OFFSETS.source.apply_pin(clipped_image),
source_clip_rect,
clipped_image.width(),
clipped_image.height(),
clipped_image.image_fit(),
Some(
sixtyfps_corelib::items::ClippedImage::FIELD_OFFSETS
.colorize
.apply_pin(clipped_image),
),
);
}
fn draw_text(&mut self, pos: Point, text: std::pin::Pin<&sixtyfps_corelib::items::Text>) {
let pos = pos + euclid::Vector2D::new(text.x(), text.y());
let max_width = text.width();
let max_height = text.height();
if max_width <= 0. || max_height <= 0. {
return;
}
let string = text.text();
let string = string.as_str();
let vertical_alignment = text.vertical_alignment();
let horizontal_alignment = text.horizontal_alignment();
let font = self.shared_data.loaded_fonts.borrow_mut().font(
&self.shared_data.canvas,
text.font_request(),
self.scale_factor,
);
let wrap = text.wrap() == TextWrap::word_wrap;
let text_size = font.text_size(string, if wrap { Some(max_width) } else { None });
let paint = match self.brush_to_paint(text.color(), &mut rect_to_path(text.geometry())) {
Some(paint) => font.init_paint(paint),
None => return,
};
let mut canvas = self.shared_data.canvas.borrow_mut();
let font_metrics = canvas.measure_font(paint).unwrap();
let mut y = pos.y
+ match vertical_alignment {
TextVerticalAlignment::top => 0.,
TextVerticalAlignment::center => max_height / 2. - text_size.height / 2.,
TextVerticalAlignment::bottom => max_height - text_size.height,
};
let mut draw_line = |canvas: &mut femtovg::Canvas<_>, to_draw: &str| {
let text_metrics = canvas.measure_text(0., 0., to_draw, paint).unwrap();
let translate_x = match horizontal_alignment {
TextHorizontalAlignment::left => 0.,
TextHorizontalAlignment::center => max_width / 2. - text_metrics.width() / 2.,
TextHorizontalAlignment::right => max_width - text_metrics.width(),
};
canvas.fill_text(pos.x + translate_x, y, to_draw, paint).unwrap();
y += font_metrics.height();
};
if wrap {
let mut start = 0;
while start < string.len() {
let index = canvas.break_text(max_width, &string[start..], paint).unwrap();
if index == 0 {
// FIXME the word is too big to be shown, but we should still break, ideally
break;
}
let index = start + index;
// trim is there to remove the \n
draw_line(&mut canvas, string[start..index].trim());
start = index;
}
} else {
let elide = text.overflow() == TextOverflow::elide;
'lines: for line in string.lines() {
let text_metrics = canvas.measure_text(0., 0., line, paint).unwrap();
if text_metrics.width() > max_width {
let w = max_width
- if elide {
canvas.measure_text(0., 0., "", paint).unwrap().width()
} else {
0.
};
let mut current_x = 0.;
for glyph in text_metrics.glyphs {
current_x += glyph.advance_x;
if current_x >= w {
let txt = &line[..glyph.byte_index];
if elide {
let elided = format!("{}", txt);
draw_line(&mut canvas, &elided);
} else {
draw_line(&mut canvas, txt);
}
continue 'lines;
}
}
}
draw_line(&mut canvas, line);
}
}
}
fn draw_text_input(
&mut self,
pos: Point,
text_input: std::pin::Pin<&sixtyfps_corelib::items::TextInput>,
) {
let width = text_input.width();
let height = text_input.height();
if width <= 0. || height <= 0. {
return;
}
let pos = pos + euclid::Vector2D::new(text_input.x(), text_input.y());
let font = self.shared_data.loaded_fonts.borrow_mut().font(
&self.shared_data.canvas,
text_input.font_request(),
self.scale_factor,
);
let paint = match self
.brush_to_paint(text_input.color(), &mut rect_to_path(text_input.geometry()))
{
Some(paint) => paint,
None => return,
};
let metrics = self.draw_text_impl(
pos,
width,
height,
&text_input.text(),
text_input.font_request(),
paint,
text_input.horizontal_alignment(),
text_input.vertical_alignment(),
);
// This way of drawing selected text isn't quite 100% correct. Due to femtovg only being able to
// have a simple rectangular selection - due to the use of the scissor clip - the selected text is
// drawn *over* the unselected text. If the selection background color is transparent, then that means
// that glyphs are blended twice, which may lead to artifacts.
// It would be better to draw the selected text and non-selected text without overlap.
if text_input.has_selection() {
let (anchor_pos, cursor_pos) = text_input.selection_anchor_and_cursor();
let mut selection_start_x = 0.;
let mut selection_end_x = 0.;
for glyph in &metrics.glyphs {
if glyph.byte_index == anchor_pos {
selection_start_x = glyph.x;
}
if glyph.byte_index == (cursor_pos as i32 - 1).max(0) as usize {
selection_end_x = glyph.x + glyph.advance_x;
}
}
let selection_rect = Rect::new(
[selection_start_x, pos.y].into(),
[selection_end_x - selection_start_x, font.height()].into(),
);
{
let mut canvas = self.shared_data.canvas.borrow_mut();
canvas.fill_path(
&mut rect_to_path(selection_rect),
femtovg::Paint::color(text_input.selection_background_color().into()),
);
canvas.save();
canvas.intersect_scissor(
selection_rect.min_x(),
selection_rect.min_y(),
selection_rect.width(),
selection_rect.height(),
);
}
self.draw_text_impl(
pos,
text_input.width(),
text_input.height(),
&text_input.text(),
text_input.font_request(),
femtovg::Paint::color(text_input.selection_foreground_color().into()),
text_input.horizontal_alignment(),
text_input.vertical_alignment(),
);
self.shared_data.canvas.borrow_mut().restore();
};
let cursor_index = text_input.cursor_position();
if cursor_index >= 0 && text_input.cursor_visible() {
let cursor_x = metrics
.glyphs
.iter()
.find_map(|glyph| {
if glyph.byte_index == cursor_index as usize {
Some(glyph.x)
} else {
None
}
})
.unwrap_or_else(|| pos.x + metrics.width());
let mut cursor_rect = femtovg::Path::new();
cursor_rect.rect(
cursor_x,
pos.y,
text_input.text_cursor_width() * self.scale_factor,
font.height(),
);
let text_paint = self.brush_to_paint(text_input.color(), &mut cursor_rect);
text_paint.map(|text_paint| {
self.shared_data.canvas.borrow_mut().fill_path(&mut cursor_rect, text_paint)
});
}
}
fn draw_path(&mut self, pos: Point, path: std::pin::Pin<&sixtyfps_corelib::items::Path>) {
let elements = path.elements();
if matches!(elements, sixtyfps_corelib::PathData::None) {
return;
}
let (offset, path_events) = path.fitted_path_events();
let mut fpath = femtovg::Path::new();
for x in path_events.iter() {
match x {
lyon_path::Event::Begin { at } => {
fpath.move_to(at.x, at.y);
}
lyon_path::Event::Line { from: _, to } => {
fpath.line_to(to.x, to.y);
}
lyon_path::Event::Quadratic { from: _, ctrl, to } => {
fpath.quad_to(ctrl.x, ctrl.y, to.x, to.y);
}
lyon_path::Event::Cubic { from: _, ctrl1, ctrl2, to } => {
fpath.bezier_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y);
}
lyon_path::Event::End { last: _, first: _, close } => {
if close {
fpath.close()
}
}
}
}
let fill_paint = self.brush_to_paint(path.fill(), &mut fpath);
let border_paint = self.brush_to_paint(path.stroke(), &mut fpath).map(|mut paint| {
paint.set_line_width(path.stroke_width());
paint
});
self.shared_data.canvas.borrow_mut().save_with(|canvas| {
canvas.translate(pos.x + path.x() + offset.x, pos.y + path.y() + offset.y);
fill_paint.map(|fill_paint| canvas.fill_path(&mut fpath, fill_paint));
border_paint.map(|border_paint| canvas.stroke_path(&mut fpath, border_paint));
})
}
/// Draws a rectangular shadow shape, which is usually placed underneath another rectangular shape
/// with an offset (the drop-shadow-offset-x/y).
/// The rendering happens in two phases:
/// * The (possibly round) rectangle is filled with the shadow color.
/// * A blurred shadow border is drawn using femtovg's box gradient. The shadow border is the
/// shape of a slightly bigger rounded rect with the inner shape subtracted. That's because
// we don't want the box gradient to visually "leak" into the inside.
fn draw_box_shadow(
&mut self,
pos: Point,
box_shadow: std::pin::Pin<&sixtyfps_corelib::items::BoxShadow>,
) {
// TODO: cache path in item to avoid re-tesselation
let blur = box_shadow.blur();
let shadow_outer_rect: euclid::Rect<f32, euclid::UnknownUnit> = euclid::rect(
box_shadow.x() + box_shadow.offset_x() - blur / 2.,
box_shadow.y() + box_shadow.offset_y() - blur / 2.,
box_shadow.width() + blur,
box_shadow.height() + blur,
);
let shadow_inner_rect: euclid::Rect<f32, euclid::UnknownUnit> = euclid::rect(
box_shadow.x() + box_shadow.offset_x() + blur / 2.,
box_shadow.y() + box_shadow.offset_y() + blur / 2.,
box_shadow.width() - blur,
box_shadow.height() - blur,
);
let shadow_fill_rect: euclid::Rect<f32, euclid::UnknownUnit> = euclid::rect(
shadow_outer_rect.min_x() + blur / 2.,
shadow_outer_rect.min_y() + blur / 2.,
box_shadow.width(),
box_shadow.height(),
);
let shadow_paint = femtovg::Paint::box_gradient(
shadow_fill_rect.min_x(),
shadow_fill_rect.min_y(),
shadow_fill_rect.width(),
shadow_fill_rect.height(),
box_shadow.border_radius(),
box_shadow.blur(),
box_shadow.color().into(),
Color::from_argb_u8(0, 0, 0, 0).into(),
);
let mut shadow_path = femtovg::Path::new();
shadow_path.rounded_rect(
shadow_outer_rect.min_x(),
shadow_outer_rect.min_y(),
shadow_outer_rect.width(),
shadow_outer_rect.height(),
box_shadow.border_radius(),
);
shadow_path.rounded_rect(
shadow_inner_rect.min_x(),
shadow_inner_rect.min_y(),
shadow_inner_rect.width(),
shadow_inner_rect.height(),
box_shadow.border_radius(),
);
shadow_path.solidity(femtovg::Solidity::Hole);
self.shared_data.canvas.borrow_mut().save_with(|canvas| {
canvas.translate(pos.x, pos.y);
let mut shadow_inner_fill_path = femtovg::Path::new();
shadow_inner_fill_path.rounded_rect(
shadow_inner_rect.min_x(),
shadow_inner_rect.min_y(),
shadow_inner_rect.width(),
shadow_inner_rect.height(),
box_shadow.border_radius() - blur / 2.,
);
let fill = femtovg::Paint::color(box_shadow.color().into());
canvas.fill_path(&mut shadow_inner_fill_path, fill);
canvas.fill_path(&mut shadow_path, shadow_paint);
})
}
fn combine_clip(&mut self, pos: Point, rect: Rect) {
let clip_rect = rect.translate([pos.x, pos.y].into());
// FIXME: Go back to using Canvas::intersect_scissor when https://github.com/femtovg/femtovg/pull/29 is merged
let final_clip_rect = if let Some(existing_clip) = self.current_scissor_rect {
existing_clip.intersection(&clip_rect).unwrap_or_default()
} else {
clip_rect
};
self.shared_data.canvas.borrow_mut().scissor(
final_clip_rect.min_x(),
final_clip_rect.min_y(),
final_clip_rect.width(),
final_clip_rect.height(),
);
self.current_scissor_rect = Some(final_clip_rect);
}
fn save_state(&mut self) {
if let Some(scissor_rect) = self.current_scissor_rect {
self.saved_scissor_rects.push(scissor_rect);
}
self.shared_data.canvas.borrow_mut().save();
}
fn restore_state(&mut self) {
self.current_scissor_rect = self.saved_scissor_rects.pop();
self.shared_data.canvas.borrow_mut().restore();
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn draw_cached_pixmap(
&mut self,
item_cache: &CachedRenderingData,
pos: Point,
update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
) {
let canvas = &self.shared_data.canvas;
let mut cache = self.shared_data.item_graphics_cache.borrow_mut();
let cache_entry = item_cache.ensure_up_to_date(&mut cache, || {
let mut cached_image = None;
update_fn(&mut |width: u32, height: u32, data: &[u8]| {
use rgb::FromSlice;
let img = imgref::Img::new(data.as_rgba(), width as usize, height as usize);
if let Some(image_id) =
canvas.borrow_mut().create_image(img, femtovg::ImageFlags::PREMULTIPLIED).ok()
{
cached_image = Some(ItemGraphicsCacheEntry::Image(Rc::new(
CachedImage::new_on_gpu(canvas, image_id, None),
)))
};
});
cached_image
});
let image_id = match cache_entry {
Some(ItemGraphicsCacheEntry::Image(image)) => image.ensure_uploaded_to_gpu(&self),
Some(ItemGraphicsCacheEntry::ColorizedImage { .. }) => unreachable!(),
None => return,
};
let mut canvas = self.shared_data.canvas.borrow_mut();
let image_info = canvas.image_info(image_id).unwrap();
let (width, height) = (image_info.width() as f32, image_info.height() as f32);
let fill_paint = femtovg::Paint::image(image_id, pos.x, pos.y, width, height, 0.0, 1.0);
let mut path = femtovg::Path::new();
path.rect(pos.x, pos.y, width, height);
canvas.fill_path(&mut path, fill_paint);
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl GLItemRenderer {
fn draw_text_impl(
&mut self,
pos: Point,
max_width: f32,
max_height: f32,
text: &str,
font_request: FontRequest,
paint: femtovg::Paint,
horizontal_alignment: TextHorizontalAlignment,
vertical_alignment: TextVerticalAlignment,
) -> femtovg::TextMetrics {
let font = self.shared_data.loaded_fonts.borrow_mut().font(
&self.shared_data.canvas,
font_request,
self.scale_factor,
);
let paint = font.init_paint(paint);
let mut canvas = self.shared_data.canvas.borrow_mut();
let (text_width, text_height) = {
let text_metrics = canvas.measure_text(0., 0., &text, paint).unwrap();
let font_metrics = canvas.measure_font(paint).unwrap();
(text_metrics.width(), font_metrics.height())
};
let translate_x = match horizontal_alignment {
TextHorizontalAlignment::left => 0.,
TextHorizontalAlignment::center => max_width / 2. - text_width / 2.,
TextHorizontalAlignment::right => max_width - text_width,
};
let translate_y = match vertical_alignment {
TextVerticalAlignment::top => 0.,
TextVerticalAlignment::center => max_height / 2. - text_height / 2.,
TextVerticalAlignment::bottom => max_height - text_height,
};
canvas.fill_text(pos.x + translate_x, pos.y + translate_y, text, paint).unwrap()
}
fn colorize_image(
&self,
image_cache_entry: ItemGraphicsCacheEntry,
colorize_property: Option<Pin<&Property<Brush>>>,
) -> ItemGraphicsCacheEntry {
let colorize_brush = match colorize_property.map_or(Brush::default(), |prop| prop.get()) {
Brush::NoBrush => return image_cache_entry,
brush => brush,
};
let image = image_cache_entry.as_image();
let image_size = match image.size() {
Some(size) => size,
None => return image_cache_entry,
};
let image_id = image.ensure_uploaded_to_gpu(&self);
let colorized_image = self
.shared_data
.canvas
.borrow_mut()
.create_image_empty(
image_size.width as _,
image_size.height as _,
femtovg::PixelFormat::Rgba8,
femtovg::ImageFlags::empty(),
)
.expect("internal error allocating temporary texture for image colorization");
let mut image_rect = femtovg::Path::new();
image_rect.rect(0., 0., image_size.width, image_size.height);
let brush_paint = self.brush_to_paint(colorize_brush, &mut image_rect).unwrap();
self.shared_data.canvas.borrow_mut().save_with(|canvas| {
canvas.set_render_target(femtovg::RenderTarget::Image(colorized_image));
canvas.global_composite_operation(femtovg::CompositeOperation::Copy);
canvas.fill_path(
&mut image_rect,
femtovg::Paint::image(
image_id,
0.,
0.,
image_size.width,
image_size.height,
0.,
1.0,
),
);
canvas.global_composite_operation(femtovg::CompositeOperation::SourceIn);
canvas.fill_path(&mut image_rect, brush_paint);
canvas.set_render_target(femtovg::RenderTarget::Screen);
});
ItemGraphicsCacheEntry::ColorizedImage {
original_image: image.clone(),
colorized_image: Rc::new(CachedImage::new_on_gpu(
&self.shared_data.canvas,
colorized_image,
None,
)),
}
}
fn draw_image_impl(
&mut self,
mut pos: Point,
item_cache: &CachedRenderingData,
source_property: std::pin::Pin<&Property<Resource>>,
source_clip_rect: Rect,
target_width: f32,
target_height: f32,
image_fit: ImageFit,
colorize_property: Option<Pin<&Property<Brush>>>,
) {
if target_width <= 0. || target_height < 0. {
return;
}
let cached_image = loop {
let image_cache_entry =
self.shared_data.load_cached_item_image_from_function(item_cache, || {
self.shared_data
.load_image_resource(source_property.get())
.map(|image| self.colorize_image(image, colorize_property))
});
// Check if the image in the cache is loaded. If not, don't draw any image and we'll return
// later when the callback from load_html_image has issued a repaint
let cached_image = match image_cache_entry {
Some(entry) if entry.as_image().size().is_some() => entry,
_ => {
return;
}
};
// It's possible that our item cache contains an image but it's not colorized yet because it was only
// placed there via the `image_size` function (which doesn't colorize). So we may have to invalidate our
// item cache and try again.
if colorize_property.map_or(false, |prop| !matches!(prop.get(), Brush::NoBrush))
&& !cached_image.is_colorized_image()
{
let mut cache = self.shared_data.item_graphics_cache.borrow_mut();
item_cache.release(&mut cache);
continue;
}
break cached_image.as_image().clone();
};
let image_id = cached_image.ensure_uploaded_to_gpu(&self);
let image_size = cached_image.size().unwrap_or_default();
let (source_width, source_height) = if source_clip_rect.is_empty() {
(image_size.width, image_size.height)
} else {
(source_clip_rect.width() as _, source_clip_rect.height() as _)
};
let mut source_x = source_clip_rect.min_x();
let mut source_y = source_clip_rect.min_y();
// The source_to_target scale is applied to the paint that holds the image as well as path
// begin rendered.
let (source_to_target_scale_x, source_to_target_scale_y) = match image_fit {
ImageFit::fill => (target_width / source_width, target_height / source_height),
ImageFit::cover => {
let ratio = f32::max(target_width / source_width, target_height / source_height);
if source_width > target_width / ratio {
source_x += (source_width - target_width / ratio) / 2.;
}
if source_height > target_height / ratio {
source_y += (source_height - target_height / ratio) / 2.
}
(ratio, ratio)
}
ImageFit::contain => {
let ratio = f32::min(target_width / source_width, target_height / source_height);
if source_width < target_width / ratio {
pos.x += (target_width - source_width * ratio) / 2.;
}
if source_height < target_height / ratio {
pos.y += (target_height - source_height * ratio) / 2.
}
(ratio, ratio)
}
};
let fill_paint = femtovg::Paint::image(
image_id,
-source_x,
-source_y,
image_size.width,
image_size.height,
0.0,
1.0,
);
let mut path = femtovg::Path::new();
path.rect(0., 0., source_width, source_height);
self.shared_data.canvas.borrow_mut().save_with(|canvas| {
canvas.translate(pos.x, pos.y);
canvas.scale(source_to_target_scale_x, source_to_target_scale_y);
canvas.fill_path(&mut path, fill_paint);
})
}
fn brush_to_paint(&self, brush: Brush, path: &mut femtovg::Path) -> Option<femtovg::Paint> {
Some(match brush {
Brush::NoBrush => return None,
Brush::SolidColor(color) => femtovg::Paint::color(color.into()),
Brush::LinearGradient(gradient) => {
let path_bounds = self.shared_data.canvas.borrow().path_bbox(path);
let path_width = path_bounds.maxx - path_bounds.minx;
let path_height = path_bounds.maxy - path_bounds.miny;
let transform = euclid::Transform2D::scale(path_width, path_height)
.then_translate(euclid::Vector2D::new(path_bounds.minx, path_bounds.miny));
let (start, end) = gradient.start_end_points();
let start: Point = transform.transform_point(start);
let end: Point = transform.transform_point(end);
let stops = gradient
.stops()
.map(|stop| (stop.position, stop.color.into()))
.collect::<Vec<_>>();
femtovg::Paint::linear_gradient_stops(start.x, start.y, end.x, end.y, &stops)
}
})
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct FontCacheKey {
family: SharedString,
weight: i32,
}
struct GLFont {
fonts: Vec<femtovg::FontId>,
pixel_size: f32,
canvas: CanvasRc,
}
impl GLFont {
fn measure(&self, text: &str) -> femtovg::TextMetrics {
self.canvas
.borrow_mut()
.measure_text(0., 0., text, self.init_paint(femtovg::Paint::default()))
.unwrap()
}
fn height(&self) -> f32 {
self.canvas
.borrow_mut()
.measure_font(self.init_paint(femtovg::Paint::default()))
.unwrap()
.height()
}
fn init_paint(&self, mut paint: femtovg::Paint) -> femtovg::Paint {
paint.set_font(&self.fonts);
paint.set_font_size(self.pixel_size);
paint.set_text_baseline(femtovg::Baseline::Top);
paint
}
fn text_size(&self, text: &str, max_width: Option<f32>) -> Size {
let paint = self.init_paint(femtovg::Paint::default());
let mut canvas = self.canvas.borrow_mut();
let font_metrics = canvas.measure_font(paint).unwrap();
let mut y = 0.;
let mut width = 0.;
let mut height = 0.;
let mut start = 0;
if let Some(max_width) = max_width {
while start < text.len() {
let index = canvas.break_text(max_width, &text[start..], paint).unwrap();
if index == 0 {
break;
}
let index = start + index;
let mesure = canvas.measure_text(0., 0., &text[start..index], paint).unwrap();
start = index;
height = y + mesure.height();
y += font_metrics.height();
width = mesure.width().max(width);
}
} else {
for line in text.lines() {
let mesure = canvas.measure_text(0., 0., line, paint).unwrap();
height = y + mesure.height();
y += font_metrics.height();
width = mesure.width().max(width);
}
}
euclid::size2(width, height)
}
}
struct GLFontMetrics {
request: FontRequest,
scale_factor: f32,
shared_data: Rc<GLRendererData>,
}
impl FontMetrics for GLFontMetrics {
fn text_size(&self, text: &str) -> Size {
self.font().text_size(text, None)
}
fn text_offset_for_x_position<'a>(&self, text: &'a str, x: f32) -> usize {
let metrics = self.font().measure(text);
let mut current_x = 0.;
for glyph in metrics.glyphs {
if current_x + glyph.advance_x / 2. >= x {
return glyph.byte_index;
}
current_x += glyph.advance_x;
}
return text.len();
}
fn height(&self) -> f32 {
self.shared_data
.canvas
.borrow_mut()
.measure_font(self.font().init_paint(femtovg::Paint::default()))
.unwrap()
.height()
}
}
impl GLFontMetrics {
fn font(&self) -> GLFont {
self.shared_data.loaded_fonts.borrow_mut().font(
&self.shared_data.canvas,
self.request.clone(),
self.scale_factor,
)
}
}
#[cfg(target_arch = "wasm32")]
pub fn create_gl_window_with_canvas_id(canvas_id: String) -> ComponentWindow {
let platform_window = GraphicsWindow::new(move |event_loop, window_builder| {
GLRenderer::new(event_loop, window_builder, &canvas_id)
});
let window = Rc::new(sixtyfps_corelib::window::Window::new(platform_window.clone()));
platform_window.self_weak.set(Rc::downgrade(&window)).ok().unwrap();
ComponentWindow(window)
}
#[doc(hidden)]
#[cold]
pub fn use_modules() {
sixtyfps_corelib::use_modules();
}
pub type NativeWidgets = ();
pub type NativeGlobals = ();
pub mod native_widgets {}
pub const HAS_NATIVE_STYLE: bool = false;
pub const IS_AVAILABLE: bool = true;
thread_local!(pub(crate) static CLIPBOARD : std::cell::RefCell<copypasta::ClipboardContext> = std::cell::RefCell::new(copypasta::ClipboardContext::new().unwrap()));
pub struct Backend;
impl sixtyfps_corelib::backend::Backend for Backend {
fn create_window(&'static self) -> ComponentWindow {
let platform_window = GraphicsWindow::new(|event_loop, window_builder| {
GLRenderer::new(
event_loop,
window_builder,
#[cfg(target_arch = "wasm32")]
"canvas",
)
});
let window = Rc::new(sixtyfps_corelib::window::Window::new(platform_window.clone()));
platform_window.self_weak.set(Rc::downgrade(&window)).ok().unwrap();
ComponentWindow(window)
}
fn run_event_loop(&'static self) {
crate::eventloop::run();
}
fn register_application_font_from_memory(
&'static self,
data: &'static [u8],
) -> Result<(), Box<dyn std::error::Error>> {
self::register_application_font_from_memory(data)
}
fn set_clipboard_text(&'static self, text: String) {
use copypasta::ClipboardProvider;
CLIPBOARD.with(|clipboard| clipboard.borrow_mut().set_contents(text).ok());
}
fn clipboard_text(&'static self) -> Option<String> {
use copypasta::ClipboardProvider;
CLIPBOARD.with(|clipboard| clipboard.borrow_mut().get_contents().ok())
}
}