Move all crates to a top-level crates folder
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
co-authored by
Max Brunsfeld
parent
d768224182
commit
fdfed3d7db
@@ -0,0 +1,29 @@
|
||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
KeyDown {
|
||||
keystroke: Keystroke,
|
||||
chars: String,
|
||||
is_held: bool,
|
||||
},
|
||||
ScrollWheel {
|
||||
position: Vector2F,
|
||||
delta: Vector2F,
|
||||
precise: bool,
|
||||
},
|
||||
LeftMouseDown {
|
||||
position: Vector2F,
|
||||
cmd: bool,
|
||||
},
|
||||
LeftMouseUp {
|
||||
position: Vector2F,
|
||||
},
|
||||
LeftMouseDragged {
|
||||
position: Vector2F,
|
||||
},
|
||||
MouseMoved {
|
||||
position: Vector2F,
|
||||
left_mouse_down: bool,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
mod atlas;
|
||||
mod dispatcher;
|
||||
mod event;
|
||||
mod fonts;
|
||||
mod geometry;
|
||||
mod image_cache;
|
||||
mod platform;
|
||||
mod renderer;
|
||||
mod sprite_cache;
|
||||
mod window;
|
||||
|
||||
use cocoa::base::{BOOL, NO, YES};
|
||||
pub use dispatcher::Dispatcher;
|
||||
pub use fonts::FontSystem;
|
||||
use platform::{MacForegroundPlatform, MacPlatform};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use window::Window;
|
||||
|
||||
pub(crate) fn platform() -> Arc<dyn super::Platform> {
|
||||
Arc::new(MacPlatform::new())
|
||||
}
|
||||
|
||||
pub(crate) fn foreground_platform() -> Rc<dyn super::ForegroundPlatform> {
|
||||
Rc::new(MacForegroundPlatform::default())
|
||||
}
|
||||
|
||||
trait BoolExt {
|
||||
fn to_objc(self) -> BOOL;
|
||||
}
|
||||
|
||||
impl BoolExt for bool {
|
||||
fn to_objc(self) -> BOOL {
|
||||
if self {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use crate::geometry::{
|
||||
rect::RectI,
|
||||
vector::{vec2i, Vector2I},
|
||||
};
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use foreign_types::ForeignType;
|
||||
use metal::{self, Device, TextureDescriptor};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
pub struct AtlasAllocator {
|
||||
device: Device,
|
||||
texture_descriptor: TextureDescriptor,
|
||||
atlases: Vec<Atlas>,
|
||||
free_atlases: Vec<Atlas>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct AllocId {
|
||||
pub atlas_id: usize,
|
||||
alloc_id: etagere::AllocId,
|
||||
}
|
||||
|
||||
impl AtlasAllocator {
|
||||
pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
|
||||
let mut me = Self {
|
||||
device,
|
||||
texture_descriptor,
|
||||
atlases: Vec::new(),
|
||||
free_atlases: Vec::new(),
|
||||
};
|
||||
let atlas = me.new_atlas(Vector2I::zero());
|
||||
me.atlases.push(atlas);
|
||||
me
|
||||
}
|
||||
|
||||
pub fn default_atlas_size(&self) -> Vector2I {
|
||||
vec2i(
|
||||
self.texture_descriptor.width() as i32,
|
||||
self.texture_descriptor.height() as i32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
|
||||
let (alloc_id, origin) = self
|
||||
.atlases
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.allocate(requested_size)
|
||||
.unwrap_or_else(|| {
|
||||
let mut atlas = self.new_atlas(requested_size);
|
||||
let (id, origin) = atlas.allocate(requested_size).unwrap();
|
||||
self.atlases.push(atlas);
|
||||
(id, origin)
|
||||
});
|
||||
|
||||
let id = AllocId {
|
||||
atlas_id: self.atlases.len() - 1,
|
||||
alloc_id,
|
||||
};
|
||||
(id, origin)
|
||||
}
|
||||
|
||||
pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
|
||||
let (alloc_id, origin) = self.allocate(size);
|
||||
let bounds = RectI::new(origin, size);
|
||||
self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
|
||||
(alloc_id, bounds)
|
||||
}
|
||||
|
||||
pub fn deallocate(&mut self, id: AllocId) {
|
||||
if let Some(atlas) = self.atlases.get_mut(id.atlas_id) {
|
||||
atlas.deallocate(id.alloc_id);
|
||||
if atlas.is_empty() {
|
||||
self.free_atlases.push(self.atlases.remove(id.atlas_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
for atlas in &mut self.atlases {
|
||||
atlas.clear();
|
||||
}
|
||||
self.free_atlases.extend(self.atlases.drain(1..));
|
||||
}
|
||||
|
||||
pub fn texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
|
||||
self.atlases.get(atlas_id).map(|a| a.texture.as_ref())
|
||||
}
|
||||
|
||||
fn new_atlas(&mut self, required_size: Vector2I) -> Atlas {
|
||||
if let Some(i) = self.free_atlases.iter().rposition(|atlas| {
|
||||
atlas.size().x() >= required_size.x() && atlas.size().y() >= required_size.y()
|
||||
}) {
|
||||
self.free_atlases.remove(i)
|
||||
} else {
|
||||
let size = self.default_atlas_size().max(required_size);
|
||||
let texture = if size.x() as u64 > self.texture_descriptor.width()
|
||||
|| size.y() as u64 > self.texture_descriptor.height()
|
||||
{
|
||||
let descriptor = unsafe {
|
||||
let descriptor_ptr: *mut metal::MTLTextureDescriptor =
|
||||
msg_send![self.texture_descriptor, copy];
|
||||
metal::TextureDescriptor::from_ptr(descriptor_ptr)
|
||||
};
|
||||
descriptor.set_width(size.x() as u64);
|
||||
descriptor.set_height(size.y() as u64);
|
||||
self.device.new_texture(&descriptor)
|
||||
} else {
|
||||
self.device.new_texture(&self.texture_descriptor)
|
||||
};
|
||||
Atlas::new(size, texture)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Atlas {
|
||||
allocator: BucketedAtlasAllocator,
|
||||
texture: metal::Texture,
|
||||
}
|
||||
|
||||
impl Atlas {
|
||||
fn new(size: Vector2I, texture: metal::Texture) -> Self {
|
||||
Self {
|
||||
allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
|
||||
texture,
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> Vector2I {
|
||||
let size = self.allocator.size();
|
||||
vec2i(size.width, size.height)
|
||||
}
|
||||
|
||||
fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> {
|
||||
let alloc = self
|
||||
.allocator
|
||||
.allocate(etagere::Size::new(size.x(), size.y()))?;
|
||||
let origin = alloc.rectangle.min;
|
||||
Some((alloc.id, vec2i(origin.x, origin.y)))
|
||||
}
|
||||
|
||||
fn upload(&mut self, bounds: RectI, bytes: &[u8]) {
|
||||
let region = metal::MTLRegion::new_2d(
|
||||
bounds.origin().x() as u64,
|
||||
bounds.origin().y() as u64,
|
||||
bounds.size().x() as u64,
|
||||
bounds.size().y() as u64,
|
||||
);
|
||||
self.texture.replace_region(
|
||||
region,
|
||||
0,
|
||||
bytes.as_ptr() as *const _,
|
||||
(bounds.size().x() * self.bytes_per_pixel() as i32) as u64,
|
||||
);
|
||||
}
|
||||
|
||||
fn bytes_per_pixel(&self) -> u8 {
|
||||
use metal::MTLPixelFormat::*;
|
||||
match self.texture.pixel_format() {
|
||||
A8Unorm | R8Unorm => 1,
|
||||
RGBA8Unorm | BGRA8Unorm => 4,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deallocate(&mut self, id: etagere::AllocId) {
|
||||
self.allocator.deallocate(id);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.allocator.is_empty()
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.allocator.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
@@ -0,0 +1,43 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use async_task::Runnable;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{BOOL, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use std::ffi::c_void;
|
||||
|
||||
use crate::platform;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
|
||||
|
||||
pub fn dispatch_get_main_queue() -> dispatch_queue_t {
|
||||
unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
|
||||
}
|
||||
|
||||
pub struct Dispatcher;
|
||||
|
||||
impl platform::Dispatcher for Dispatcher {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
|
||||
is_main_thread == YES
|
||||
}
|
||||
|
||||
fn run_on_main_thread(&self, runnable: Runnable) {
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_main_queue(),
|
||||
runnable.into_raw() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
}
|
||||
|
||||
extern "C" fn trampoline(runnable: *mut c_void) {
|
||||
let task = unsafe { Runnable::from_raw(runnable as *mut ()) };
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event};
|
||||
use cocoa::appkit::{
|
||||
NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY,
|
||||
NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY,
|
||||
NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY,
|
||||
NSUpArrowFunctionKey as ARROW_UP_KEY,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
||||
base::{id, nil, YES},
|
||||
foundation::NSString as _,
|
||||
};
|
||||
use std::{ffi::CStr, os::raw::c_char};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const ENTER_KEY: u16 = 0x0d;
|
||||
const ESCAPE_KEY: u16 = 0x1b;
|
||||
const TAB_KEY: u16 = 0x09;
|
||||
|
||||
impl Event {
|
||||
pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
|
||||
let event_type = native_event.eventType();
|
||||
|
||||
// Filter out event types that aren't in the NSEventType enum.
|
||||
// See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
|
||||
match event_type as u64 {
|
||||
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match event_type {
|
||||
NSEventType::NSKeyDown => {
|
||||
let modifiers = native_event.modifierFlags();
|
||||
let unmodified_chars = native_event.charactersIgnoringModifiers();
|
||||
let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
|
||||
match first_char as u16 {
|
||||
ARROW_UP_KEY => "up",
|
||||
ARROW_DOWN_KEY => "down",
|
||||
ARROW_LEFT_KEY => "left",
|
||||
ARROW_RIGHT_KEY => "right",
|
||||
PAGE_UP_KEY => "pageup",
|
||||
PAGE_DOWN_KEY => "pagedown",
|
||||
BACKSPACE_KEY => "backspace",
|
||||
ENTER_KEY => "enter",
|
||||
DELETE_KEY => "delete",
|
||||
ESCAPE_KEY => "escape",
|
||||
TAB_KEY => "tab",
|
||||
_ => unmodified_chars,
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let chars = native_event.characters();
|
||||
let chars = CStr::from_ptr(chars.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
Some(Self::KeyDown {
|
||||
keystroke: Keystroke {
|
||||
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
key: unmodified_chars.into(),
|
||||
},
|
||||
chars,
|
||||
is_held: native_event.isARepeat() == YES,
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseDown => {
|
||||
window_height.map(|window_height| Self::LeftMouseDown {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
}),
|
||||
NSEventType::NSLeftMouseDragged => {
|
||||
window_height.map(|window_height| Self::LeftMouseDragged {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
})
|
||||
}
|
||||
NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
delta: vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
}),
|
||||
NSEventType::NSMouseMoved => window_height.map(|window_height| Self::MouseMoved {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
use crate::{
|
||||
fonts::{FontId, GlyphId, Metrics, Properties},
|
||||
geometry::{
|
||||
rect::{RectF, RectI},
|
||||
transform2d::Transform2F,
|
||||
vector::{vec2f, vec2i, Vector2F},
|
||||
},
|
||||
platform,
|
||||
text_layout::{Glyph, LineLayout, Run, RunStyle},
|
||||
};
|
||||
use cocoa::appkit::{CGFloat, CGPoint};
|
||||
use core_foundation::{
|
||||
array::CFIndex,
|
||||
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
|
||||
base::{CFRange, TCFType},
|
||||
number::CFNumber,
|
||||
string::CFString,
|
||||
};
|
||||
use core_graphics::{
|
||||
base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
|
||||
};
|
||||
use core_text::{line::CTLine, string_attributes::kCTFontAttributeName};
|
||||
use font_kit::{
|
||||
canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource,
|
||||
sources::mem::MemSource,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kCGImageAlphaOnly: u32 = 7;
|
||||
|
||||
pub struct FontSystem(RwLock<FontSystemState>);
|
||||
|
||||
struct FontSystemState {
|
||||
memory_source: MemSource,
|
||||
system_source: SystemSource,
|
||||
fonts: Vec<font_kit::font::Font>,
|
||||
}
|
||||
|
||||
impl FontSystem {
|
||||
pub fn new() -> Self {
|
||||
Self(RwLock::new(FontSystemState {
|
||||
memory_source: MemSource::empty(),
|
||||
system_source: SystemSource::new(),
|
||||
fonts: Vec::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::FontSystem for FontSystem {
|
||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
|
||||
self.0.write().add_fonts(fonts)
|
||||
}
|
||||
|
||||
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
|
||||
self.0.write().load_family(name)
|
||||
}
|
||||
|
||||
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
|
||||
self.0.read().select_font(font_ids, properties)
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> Metrics {
|
||||
self.0.read().font_metrics(font_id)
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
|
||||
self.0.read().typographic_bounds(font_id, glyph_id)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.0.read().glyph_for_char(font_id, ch)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
subpixel_shift: Vector2F,
|
||||
scale_factor: f32,
|
||||
) -> Option<(RectI, Vec<u8>)> {
|
||||
self.0
|
||||
.read()
|
||||
.rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor)
|
||||
}
|
||||
|
||||
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
|
||||
self.0.read().layout_line(text, font_size, runs)
|
||||
}
|
||||
|
||||
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize> {
|
||||
self.0.read().wrap_line(text, font_id, font_size, width)
|
||||
}
|
||||
}
|
||||
|
||||
impl FontSystemState {
|
||||
fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
|
||||
self.memory_source.add_fonts(
|
||||
fonts
|
||||
.iter()
|
||||
.map(|bytes| Handle::from_memory(bytes.clone(), 0)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
|
||||
let mut font_ids = Vec::new();
|
||||
|
||||
let family = self
|
||||
.memory_source
|
||||
.select_family_by_name(name)
|
||||
.or_else(|_| self.system_source.select_family_by_name(name))?;
|
||||
for font in family.fonts() {
|
||||
let font = font.load()?;
|
||||
font_ids.push(FontId(self.fonts.len()));
|
||||
self.fonts.push(font);
|
||||
}
|
||||
Ok(font_ids)
|
||||
}
|
||||
|
||||
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
|
||||
let candidates = font_ids
|
||||
.iter()
|
||||
.map(|font_id| self.fonts[font_id.0].properties())
|
||||
.collect::<Vec<_>>();
|
||||
let idx = font_kit::matching::find_best_match(&candidates, properties)?;
|
||||
Ok(font_ids[idx])
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> Metrics {
|
||||
self.fonts[font_id.0].metrics()
|
||||
}
|
||||
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
|
||||
Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
self.fonts[font_id.0].glyph_for_char(ch)
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
subpixel_shift: Vector2F,
|
||||
scale_factor: f32,
|
||||
) -> Option<(RectI, Vec<u8>)> {
|
||||
let font = &self.fonts[font_id.0];
|
||||
let scale = Transform2F::from_scale(scale_factor);
|
||||
let bounds = font
|
||||
.raster_bounds(
|
||||
glyph_id,
|
||||
font_size,
|
||||
scale,
|
||||
HintingOptions::None,
|
||||
RasterizationOptions::GrayscaleAa,
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
if bounds.width() == 0 || bounds.height() == 0 {
|
||||
None
|
||||
} else {
|
||||
// Make room for subpixel variants.
|
||||
let bounds = RectI::new(bounds.origin(), bounds.size() + vec2i(1, 1));
|
||||
let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
|
||||
let cx = CGContext::create_bitmap_context(
|
||||
Some(pixels.as_mut_ptr() as *mut _),
|
||||
bounds.width() as usize,
|
||||
bounds.height() as usize,
|
||||
8,
|
||||
bounds.width() as usize,
|
||||
&CGColorSpace::create_device_gray(),
|
||||
kCGImageAlphaOnly,
|
||||
);
|
||||
|
||||
// Move the origin to bottom left and account for scaling, this
|
||||
// makes drawing text consistent with the font-kit's raster_bounds.
|
||||
cx.translate(0.0, bounds.height() as CGFloat);
|
||||
let transform = scale.translate(-bounds.origin().to_f32());
|
||||
cx.set_text_matrix(&CGAffineTransform {
|
||||
a: transform.matrix.m11() as CGFloat,
|
||||
b: -transform.matrix.m21() as CGFloat,
|
||||
c: -transform.matrix.m12() as CGFloat,
|
||||
d: transform.matrix.m22() as CGFloat,
|
||||
tx: transform.vector.x() as CGFloat,
|
||||
ty: -transform.vector.y() as CGFloat,
|
||||
});
|
||||
|
||||
cx.set_font(&font.native_font().copy_to_CGFont());
|
||||
cx.set_font_size(font_size as CGFloat);
|
||||
cx.show_glyphs_at_positions(
|
||||
&[glyph_id as CGGlyph],
|
||||
&[CGPoint::new(
|
||||
(subpixel_shift.x() / scale_factor) as CGFloat,
|
||||
(subpixel_shift.y() / scale_factor) as CGFloat,
|
||||
)],
|
||||
);
|
||||
|
||||
Some((bounds, pixels))
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
|
||||
let font_id_attr_name = CFString::from_static_string("zed_font_id");
|
||||
|
||||
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
|
||||
let mut string = CFMutableAttributedString::new();
|
||||
{
|
||||
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
|
||||
let utf16_line_len = string.char_len() as usize;
|
||||
|
||||
let last_run: RefCell<Option<(usize, FontId)>> = Default::default();
|
||||
let font_runs = runs
|
||||
.iter()
|
||||
.filter_map(|(len, style)| {
|
||||
let mut last_run = last_run.borrow_mut();
|
||||
if let Some((last_len, last_font_id)) = last_run.as_mut() {
|
||||
if style.font_id == *last_font_id {
|
||||
*last_len += *len;
|
||||
None
|
||||
} else {
|
||||
let result = (*last_len, *last_font_id);
|
||||
*last_len = *len;
|
||||
*last_font_id = style.font_id;
|
||||
Some(result)
|
||||
}
|
||||
} else {
|
||||
*last_run = Some((*len, style.font_id));
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(std::iter::from_fn(|| last_run.borrow_mut().take()));
|
||||
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
for (run_len, font_id) in font_runs {
|
||||
let utf8_end = ix_converter.utf8_ix + run_len;
|
||||
let utf16_start = ix_converter.utf16_ix;
|
||||
|
||||
if utf16_start >= utf16_line_len {
|
||||
break;
|
||||
}
|
||||
|
||||
ix_converter.advance_to_utf8_ix(utf8_end);
|
||||
let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
|
||||
|
||||
let cf_range =
|
||||
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
|
||||
let font = &self.fonts[font_id.0];
|
||||
unsafe {
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
kCTFontAttributeName,
|
||||
&font.native_font().clone_with_font_size(font_size as f64),
|
||||
);
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
font_id_attr_name.as_concrete_TypeRef(),
|
||||
&CFNumber::from(font_id.0 as i64),
|
||||
);
|
||||
}
|
||||
|
||||
if utf16_end == utf16_line_len {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
|
||||
let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
|
||||
|
||||
let mut runs = Vec::new();
|
||||
for run in line.glyph_runs().into_iter() {
|
||||
let font_id = FontId(
|
||||
run.attributes()
|
||||
.unwrap()
|
||||
.get(&font_id_attr_name)
|
||||
.downcast::<CFNumber>()
|
||||
.unwrap()
|
||||
.to_i64()
|
||||
.unwrap() as usize,
|
||||
);
|
||||
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
let mut glyphs = Vec::new();
|
||||
for ((glyph_id, position), glyph_utf16_ix) in run
|
||||
.glyphs()
|
||||
.iter()
|
||||
.zip(run.positions().iter())
|
||||
.zip(run.string_indices().iter())
|
||||
{
|
||||
let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
|
||||
ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
|
||||
glyphs.push(Glyph {
|
||||
id: *glyph_id as GlyphId,
|
||||
position: vec2f(position.x as f32, position.y as f32),
|
||||
index: ix_converter.utf8_ix,
|
||||
});
|
||||
}
|
||||
|
||||
runs.push(Run { font_id, glyphs })
|
||||
}
|
||||
|
||||
let typographic_bounds = line.get_typographic_bounds();
|
||||
LineLayout {
|
||||
width: typographic_bounds.width as f32,
|
||||
ascent: typographic_bounds.ascent as f32,
|
||||
descent: typographic_bounds.descent as f32,
|
||||
runs,
|
||||
font_size,
|
||||
len: text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize> {
|
||||
let mut string = CFMutableAttributedString::new();
|
||||
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
|
||||
let cf_range = CFRange::init(0 as isize, text.encode_utf16().count() as isize);
|
||||
let font = &self.fonts[font_id.0];
|
||||
unsafe {
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
kCTFontAttributeName,
|
||||
&font.native_font().clone_with_font_size(font_size as f64),
|
||||
);
|
||||
|
||||
let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef());
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
let mut break_indices = Vec::new();
|
||||
while ix_converter.utf8_ix < text.len() {
|
||||
let utf16_len = CTTypesetterSuggestLineBreak(
|
||||
typesetter,
|
||||
ix_converter.utf16_ix as isize,
|
||||
width as f64,
|
||||
) as usize;
|
||||
ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
|
||||
if ix_converter.utf8_ix >= text.len() {
|
||||
break;
|
||||
}
|
||||
break_indices.push(ix_converter.utf8_ix as usize);
|
||||
}
|
||||
break_indices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StringIndexConverter<'a> {
|
||||
text: &'a str,
|
||||
utf8_ix: usize,
|
||||
utf16_ix: usize,
|
||||
}
|
||||
|
||||
impl<'a> StringIndexConverter<'a> {
|
||||
fn new(text: &'a str) -> Self {
|
||||
Self {
|
||||
text,
|
||||
utf8_ix: 0,
|
||||
utf16_ix: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_to_utf8_ix(&mut self, utf8_target: usize) {
|
||||
for (ix, c) in self.text[self.utf8_ix..].char_indices() {
|
||||
if self.utf8_ix + ix >= utf8_target {
|
||||
self.utf8_ix += ix;
|
||||
return;
|
||||
}
|
||||
self.utf16_ix += c.len_utf16();
|
||||
}
|
||||
self.utf8_ix = self.text.len();
|
||||
}
|
||||
|
||||
fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
|
||||
for (ix, c) in self.text[self.utf8_ix..].char_indices() {
|
||||
if self.utf16_ix >= utf16_target {
|
||||
self.utf8_ix += ix;
|
||||
return;
|
||||
}
|
||||
self.utf16_ix += c.len_utf16();
|
||||
}
|
||||
self.utf8_ix = self.text.len();
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct __CFTypesetter(c_void);
|
||||
|
||||
pub type CTTypesetterRef = *const __CFTypesetter;
|
||||
|
||||
#[link(name = "CoreText", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef;
|
||||
|
||||
fn CTTypesetterSuggestLineBreak(
|
||||
typesetter: CTTypesetterRef,
|
||||
start_index: CFIndex,
|
||||
width: f64,
|
||||
) -> CFIndex;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MutableAppContext;
|
||||
use font_kit::properties::{Style, Weight};
|
||||
use platform::FontSystem as _;
|
||||
|
||||
#[crate::test(self, retries = 5)]
|
||||
fn test_layout_str(_: &mut MutableAppContext) {
|
||||
// This is failing intermittently on CI and we don't have time to figure it out
|
||||
let fonts = FontSystem::new();
|
||||
let menlo = fonts.load_family("Menlo").unwrap();
|
||||
let menlo_regular = RunStyle {
|
||||
font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
};
|
||||
let menlo_italic = RunStyle {
|
||||
font_id: fonts
|
||||
.select_font(&menlo, &Properties::new().style(Style::Italic))
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
};
|
||||
let menlo_bold = RunStyle {
|
||||
font_id: fonts
|
||||
.select_font(&menlo, &Properties::new().weight(Weight::BOLD))
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
};
|
||||
assert_ne!(menlo_regular, menlo_italic);
|
||||
assert_ne!(menlo_regular, menlo_bold);
|
||||
assert_ne!(menlo_italic, menlo_bold);
|
||||
|
||||
let line = fonts.layout_line(
|
||||
"hello world",
|
||||
16.0,
|
||||
&[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
|
||||
);
|
||||
assert_eq!(line.runs.len(), 3);
|
||||
assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
|
||||
assert_eq!(line.runs[0].glyphs.len(), 2);
|
||||
assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
|
||||
assert_eq!(line.runs[1].glyphs.len(), 4);
|
||||
assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
|
||||
assert_eq!(line.runs[2].glyphs.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glyph_offsets() -> anyhow::Result<()> {
|
||||
let fonts = FontSystem::new();
|
||||
let zapfino = fonts.load_family("Zapfino")?;
|
||||
let zapfino_regular = RunStyle {
|
||||
font_id: fonts.select_font(&zapfino, &Properties::new())?,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
};
|
||||
let menlo = fonts.load_family("Menlo")?;
|
||||
let menlo_regular = RunStyle {
|
||||
font_id: fonts.select_font(&menlo, &Properties::new())?,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
};
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
let line = fonts.layout_line(
|
||||
text,
|
||||
16.0,
|
||||
&[
|
||||
(9, zapfino_regular),
|
||||
(13, menlo_regular),
|
||||
(text.len() - 22, zapfino_regular),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
line.runs
|
||||
.iter()
|
||||
.flat_map(|r| r.glyphs.iter())
|
||||
.map(|g| g.index)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_rasterize_glyph() {
|
||||
use std::{fs::File, io::BufWriter, path::Path};
|
||||
|
||||
let fonts = FontSystem::new();
|
||||
let font_ids = fonts.load_family("Fira Code").unwrap();
|
||||
let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
|
||||
let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
|
||||
|
||||
const VARIANTS: usize = 1;
|
||||
for i in 0..VARIANTS {
|
||||
let variant = i as f32 / VARIANTS as f32;
|
||||
let (bounds, bytes) = fonts
|
||||
.rasterize_glyph(font_id, 16.0, glyph_id, vec2f(variant, variant), 2.)
|
||||
.unwrap();
|
||||
|
||||
let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
|
||||
let path = Path::new(&name);
|
||||
let file = File::create(path).unwrap();
|
||||
let ref mut w = BufWriter::new(file);
|
||||
|
||||
let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
|
||||
encoder.set_color(png::ColorType::Grayscale);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header().unwrap();
|
||||
writer.write_image_data(&bytes).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_line() {
|
||||
let fonts = FontSystem::new();
|
||||
let font_ids = fonts.load_family("Helvetica").unwrap();
|
||||
let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
|
||||
|
||||
let line = "one two three four five\n";
|
||||
let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
|
||||
assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
|
||||
|
||||
let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
|
||||
let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
|
||||
assert_eq!(
|
||||
wrap_boundaries,
|
||||
&["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_line_bom_char() {
|
||||
let fonts = FontSystem::new();
|
||||
let font_ids = fonts.load_family("Helvetica").unwrap();
|
||||
let style = RunStyle {
|
||||
font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
};
|
||||
|
||||
let line = "\u{feff}";
|
||||
let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
|
||||
assert_eq!(layout.len, line.len());
|
||||
assert!(layout.runs.is_empty());
|
||||
|
||||
let line = "a\u{feff}b";
|
||||
let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
|
||||
assert_eq!(layout.len, line.len());
|
||||
assert_eq!(layout.runs.len(), 1);
|
||||
assert_eq!(layout.runs[0].glyphs.len(), 2);
|
||||
assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
|
||||
// There's no glyph for \u{feff}
|
||||
assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use cocoa::foundation::{NSPoint, NSRect, NSSize};
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
|
||||
pub trait Vector2FExt {
|
||||
fn to_ns_point(&self) -> NSPoint;
|
||||
fn to_ns_size(&self) -> NSSize;
|
||||
}
|
||||
|
||||
pub trait RectFExt {
|
||||
fn to_ns_rect(&self) -> NSRect;
|
||||
}
|
||||
|
||||
impl Vector2FExt for Vector2F {
|
||||
fn to_ns_point(&self) -> NSPoint {
|
||||
NSPoint::new(self.x() as f64, self.y() as f64)
|
||||
}
|
||||
|
||||
fn to_ns_size(&self) -> NSSize {
|
||||
NSSize::new(self.x() as f64, self.y() as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl RectFExt for RectF {
|
||||
fn to_ns_rect(&self) -> NSRect {
|
||||
NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
|
||||
|
||||
use super::atlas::{AllocId, AtlasAllocator};
|
||||
use crate::{
|
||||
geometry::{rect::RectI, vector::Vector2I},
|
||||
ImageData,
|
||||
};
|
||||
use std::{collections::HashMap, mem};
|
||||
|
||||
pub struct ImageCache {
|
||||
prev_frame: HashMap<usize, (AllocId, RectI)>,
|
||||
curr_frame: HashMap<usize, (AllocId, RectI)>,
|
||||
atlases: AtlasAllocator,
|
||||
}
|
||||
|
||||
impl ImageCache {
|
||||
pub fn new(device: metal::Device, size: Vector2I) -> Self {
|
||||
let descriptor = TextureDescriptor::new();
|
||||
descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
descriptor.set_width(size.x() as u64);
|
||||
descriptor.set_height(size.y() as u64);
|
||||
Self {
|
||||
prev_frame: Default::default(),
|
||||
curr_frame: Default::default(),
|
||||
atlases: AtlasAllocator::new(device, descriptor),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, image: &ImageData) -> (AllocId, RectI) {
|
||||
let (alloc_id, atlas_bounds) = self
|
||||
.prev_frame
|
||||
.remove(&image.id)
|
||||
.or_else(|| self.curr_frame.get(&image.id).copied())
|
||||
.unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes()));
|
||||
self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
|
||||
(alloc_id, atlas_bounds)
|
||||
}
|
||||
|
||||
pub fn finish_frame(&mut self) {
|
||||
mem::swap(&mut self.prev_frame, &mut self.curr_frame);
|
||||
for (_, (id, _)) in self.curr_frame.drain() {
|
||||
self.atlases.deallocate(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&TextureRef> {
|
||||
self.atlases.texture(atlas_id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
|
||||
use crate::{
|
||||
executor,
|
||||
keymap::Keystroke,
|
||||
platform::{self, CursorStyle},
|
||||
AnyAction, ClipboardItem, Event, Menu, MenuItem,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, selector, YES},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
|
||||
};
|
||||
use core_foundation::{
|
||||
base::{CFType, CFTypeRef, OSStatus, TCFType as _},
|
||||
boolean::CFBoolean,
|
||||
data::CFData,
|
||||
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
|
||||
string::{CFString, CFStringRef},
|
||||
};
|
||||
use ctor::ctor;
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
runtime::{Class, Object, Sel},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use ptr::null_mut;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
convert::TryInto,
|
||||
ffi::{c_void, CStr},
|
||||
os::raw::c_char,
|
||||
path::{Path, PathBuf},
|
||||
ptr,
|
||||
rc::Rc,
|
||||
slice, str,
|
||||
sync::Arc,
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
const MAC_PLATFORM_IVAR: &'static str = "platform";
|
||||
static mut APP_CLASS: *const Class = ptr::null();
|
||||
static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
|
||||
|
||||
#[ctor]
|
||||
unsafe fn build_classes() {
|
||||
APP_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
|
||||
decl.add_method(
|
||||
sel!(sendEvent:),
|
||||
send_event as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.register()
|
||||
};
|
||||
|
||||
APP_DELEGATE_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
|
||||
decl.add_method(
|
||||
sel!(applicationDidFinishLaunching:),
|
||||
did_finish_launching as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationDidBecomeActive:),
|
||||
did_become_active as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationDidResignActive:),
|
||||
did_resign_active as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(handleGPUIMenuItem:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(application:openFiles:),
|
||||
open_files as extern "C" fn(&mut Object, Sel, id, id),
|
||||
);
|
||||
decl.register()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MacForegroundPlatformState {
|
||||
become_active: Option<Box<dyn FnMut()>>,
|
||||
resign_active: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
|
||||
open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
|
||||
finish_launching: Option<Box<dyn FnOnce() -> ()>>,
|
||||
menu_actions: Vec<Box<dyn AnyAction>>,
|
||||
}
|
||||
|
||||
impl MacForegroundPlatform {
|
||||
unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
|
||||
let menu_bar = NSMenu::new(nil).autorelease();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
state.menu_actions.clear();
|
||||
|
||||
for menu_config in menus {
|
||||
let menu_bar_item = NSMenuItem::new(nil).autorelease();
|
||||
let menu = NSMenu::new(nil).autorelease();
|
||||
let menu_name = menu_config.name;
|
||||
|
||||
menu.setTitle_(ns_string(menu_name));
|
||||
|
||||
for item_config in menu_config.items {
|
||||
let item;
|
||||
|
||||
match item_config {
|
||||
MenuItem::Separator => {
|
||||
item = NSMenuItem::separatorItem(nil);
|
||||
}
|
||||
MenuItem::Action {
|
||||
name,
|
||||
keystroke,
|
||||
action,
|
||||
} => {
|
||||
if let Some(keystroke) = keystroke {
|
||||
let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Invalid keystroke for menu item {}:{} - {:?}",
|
||||
menu_name, name, err
|
||||
)
|
||||
});
|
||||
|
||||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
|
||||
(keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
|
||||
(keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
|
||||
] {
|
||||
if *modifier {
|
||||
mask |= *flag;
|
||||
}
|
||||
}
|
||||
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
ns_string(&keystroke.key),
|
||||
)
|
||||
.autorelease();
|
||||
item.setKeyEquivalentModifierMask_(mask);
|
||||
} else {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
}
|
||||
|
||||
let tag = state.menu_actions.len() as NSInteger;
|
||||
let _: () = msg_send![item, setTag: tag];
|
||||
state.menu_actions.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
menu.addItem_(item);
|
||||
}
|
||||
|
||||
menu_bar_item.setSubmenu_(menu);
|
||||
menu_bar.addItem_(menu_bar_item);
|
||||
}
|
||||
|
||||
menu_bar
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::ForegroundPlatform for MacForegroundPlatform {
|
||||
fn on_become_active(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().become_active = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().resign_active = Some(callback);
|
||||
}
|
||||
|
||||
fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
|
||||
self.0.borrow_mut().event = Some(callback);
|
||||
}
|
||||
|
||||
fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
|
||||
self.0.borrow_mut().open_files = Some(callback);
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
|
||||
self.0.borrow_mut().finish_launching = Some(on_finish_launching);
|
||||
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
|
||||
app.setDelegate_(app_delegate);
|
||||
|
||||
let self_ptr = self as *const Self as *const c_void;
|
||||
(*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
|
||||
(*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
|
||||
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
app.run();
|
||||
pool.drain();
|
||||
|
||||
(*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
|
||||
(*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
|
||||
}
|
||||
}
|
||||
|
||||
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
|
||||
self.0.borrow_mut().menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>) {
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setMainMenu_(self.create_menu_bar(menus));
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: platform::PathPromptOptions,
|
||||
done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
|
||||
) {
|
||||
unsafe {
|
||||
let panel = NSOpenPanel::openPanel(nil);
|
||||
panel.setCanChooseDirectories_(options.directories.to_objc());
|
||||
panel.setCanChooseFiles_(options.files.to_objc());
|
||||
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
||||
panel.setResolvesAliases_(false.to_objc());
|
||||
let done_fn = Cell::new(Some(done_fn));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let result = if response == NSModalResponse::NSModalResponseOk {
|
||||
let mut result = Vec::new();
|
||||
let urls = panel.URLs();
|
||||
for i in 0..urls.count() {
|
||||
let url = urls.objectAtIndex(i);
|
||||
if url.isFileURL() == YES {
|
||||
let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
result.push(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(done_fn) = done_fn.take() {
|
||||
(done_fn)(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &Path,
|
||||
done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
|
||||
) {
|
||||
unsafe {
|
||||
let panel = NSSavePanel::savePanel(nil);
|
||||
let path = ns_string(directory.to_string_lossy().as_ref());
|
||||
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
|
||||
panel.setDirectoryURL(url);
|
||||
|
||||
let done_fn = Cell::new(Some(done_fn));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let result = if response == NSModalResponse::NSModalResponseOk {
|
||||
let url = panel.URL();
|
||||
if url.isFileURL() == YES {
|
||||
let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Some(PathBuf::from(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(done_fn) = done_fn.take() {
|
||||
(done_fn)(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MacPlatform {
|
||||
dispatcher: Arc<Dispatcher>,
|
||||
fonts: Arc<FontSystem>,
|
||||
pasteboard: id,
|
||||
text_hash_pasteboard_type: id,
|
||||
metadata_pasteboard_type: id,
|
||||
}
|
||||
|
||||
impl MacPlatform {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
dispatcher: Arc::new(Dispatcher),
|
||||
fonts: Arc::new(FontSystem::new()),
|
||||
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
|
||||
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
|
||||
let data = self.pasteboard.dataForType(kind);
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
Some(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MacPlatform {}
|
||||
unsafe impl Sync for MacPlatform {}
|
||||
|
||||
impl platform::Platform for MacPlatform {
|
||||
fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
|
||||
self.dispatcher.clone()
|
||||
}
|
||||
|
||||
fn activate(&self, ignoring_other_apps: bool) {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
|
||||
}
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
id: usize,
|
||||
options: platform::WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
) -> Box<dyn platform::Window> {
|
||||
Box::new(Window::open(id, options, executor, self.fonts()))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
Window::key_window_id()
|
||||
}
|
||||
|
||||
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
|
||||
self.fonts.clone()
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
// Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
|
||||
// synchronously before this method terminates. If we call `Platform::quit` while holding a
|
||||
// borrow of the app state (which most of the time we will do), we will end up
|
||||
// double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
|
||||
// this, we make quitting the application asynchronous so that we aren't holding borrows to
|
||||
// the app state on the stack when we actually terminate the app.
|
||||
|
||||
use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
|
||||
|
||||
unsafe {
|
||||
dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn quit(_: *mut c_void) {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let _: () = msg_send![app, terminate: nil];
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
unsafe {
|
||||
self.pasteboard.clearContents();
|
||||
|
||||
let text_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
item.text.as_ptr() as *const c_void,
|
||||
item.text.len() as u64,
|
||||
);
|
||||
self.pasteboard
|
||||
.setData_forType(text_bytes, NSPasteboardTypeString);
|
||||
|
||||
if let Some(metadata) = item.metadata.as_ref() {
|
||||
let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
|
||||
let hash_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
hash_bytes.as_ptr() as *const c_void,
|
||||
hash_bytes.len() as u64,
|
||||
);
|
||||
self.pasteboard
|
||||
.setData_forType(hash_bytes, self.text_hash_pasteboard_type);
|
||||
|
||||
let metadata_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
metadata.as_ptr() as *const c_void,
|
||||
metadata.len() as u64,
|
||||
);
|
||||
self.pasteboard
|
||||
.setData_forType(metadata_bytes, self.metadata_pasteboard_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
unsafe {
|
||||
if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
|
||||
let text = String::from_utf8_lossy(&text_bytes).to_string();
|
||||
let hash_bytes = self
|
||||
.read_from_pasteboard(self.text_hash_pasteboard_type)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.map(u64::from_be_bytes);
|
||||
let metadata_bytes = self
|
||||
.read_from_pasteboard(self.metadata_pasteboard_type)
|
||||
.and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
|
||||
|
||||
if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
|
||||
if hash == ClipboardItem::text_hash(&text) {
|
||||
Some(ClipboardItem {
|
||||
text,
|
||||
metadata: Some(metadata),
|
||||
})
|
||||
} else {
|
||||
Some(ClipboardItem {
|
||||
text,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Some(ClipboardItem {
|
||||
text,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
unsafe {
|
||||
let url = NSURL::alloc(nil)
|
||||
.initWithString_(ns_string(url))
|
||||
.autorelease();
|
||||
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
|
||||
msg_send![workspace, openURL: url]
|
||||
}
|
||||
}
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
|
||||
let url = CFString::from(url);
|
||||
let username = CFString::from(username);
|
||||
let password = CFData::from_buffer(password);
|
||||
|
||||
unsafe {
|
||||
use security::*;
|
||||
|
||||
// First, check if there are already credentials for the given server. If so, then
|
||||
// update the username and password.
|
||||
let mut verb = "updating";
|
||||
let mut query_attrs = CFMutableDictionary::with_capacity(2);
|
||||
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
|
||||
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
|
||||
|
||||
let mut attrs = CFMutableDictionary::with_capacity(4);
|
||||
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
|
||||
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
|
||||
attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
|
||||
attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
|
||||
|
||||
let mut status = SecItemUpdate(
|
||||
query_attrs.as_concrete_TypeRef(),
|
||||
attrs.as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
// If there were no existing credentials for the given server, then create them.
|
||||
if status == errSecItemNotFound {
|
||||
verb = "creating";
|
||||
status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
|
||||
}
|
||||
|
||||
if status != errSecSuccess {
|
||||
return Err(anyhow!("{} password failed: {}", verb, status));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
|
||||
let url = CFString::from(url);
|
||||
let cf_true = CFBoolean::true_value().as_CFTypeRef();
|
||||
|
||||
unsafe {
|
||||
use security::*;
|
||||
|
||||
// Find any credentials for the given server URL.
|
||||
let mut attrs = CFMutableDictionary::with_capacity(5);
|
||||
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
|
||||
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
|
||||
attrs.set(kSecReturnAttributes as *const _, cf_true);
|
||||
attrs.set(kSecReturnData as *const _, cf_true);
|
||||
|
||||
let mut result = CFTypeRef::from(ptr::null_mut());
|
||||
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
|
||||
match status {
|
||||
security::errSecSuccess => {}
|
||||
security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
|
||||
_ => return Err(anyhow!("reading password failed: {}", status)),
|
||||
}
|
||||
|
||||
let result = CFType::wrap_under_create_rule(result)
|
||||
.downcast::<CFDictionary>()
|
||||
.ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
|
||||
let username = result
|
||||
.find(kSecAttrAccount as *const _)
|
||||
.ok_or_else(|| anyhow!("account was missing from keychain item"))?;
|
||||
let username = CFType::wrap_under_get_rule(*username)
|
||||
.downcast::<CFString>()
|
||||
.ok_or_else(|| anyhow!("account was not a string"))?;
|
||||
let password = result
|
||||
.find(kSecValueData as *const _)
|
||||
.ok_or_else(|| anyhow!("password was missing from keychain item"))?;
|
||||
let password = CFType::wrap_under_get_rule(*password)
|
||||
.downcast::<CFData>()
|
||||
.ok_or_else(|| anyhow!("password was not a string"))?;
|
||||
|
||||
Ok(Some((username.to_string(), password.bytes().to_vec())))
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, url: &str) -> Result<()> {
|
||||
let url = CFString::from(url);
|
||||
|
||||
unsafe {
|
||||
use security::*;
|
||||
|
||||
let mut query_attrs = CFMutableDictionary::with_capacity(2);
|
||||
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
|
||||
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
|
||||
|
||||
let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
|
||||
|
||||
if status != errSecSuccess {
|
||||
return Err(anyhow!("delete password failed: {}", status));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
unsafe {
|
||||
let cursor: id = match style {
|
||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
};
|
||||
let _: () = msg_send![cursor, set];
|
||||
}
|
||||
}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
unsafe {
|
||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
||||
let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
|
||||
UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
|
||||
let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
|
||||
assert!(!platform_ptr.is_null());
|
||||
&*(platform_ptr as *const MacForegroundPlatform)
|
||||
}
|
||||
|
||||
extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
|
||||
unsafe {
|
||||
if let Some(event) = Event::from_native(native_event, None) {
|
||||
let platform = get_foreground_platform(this);
|
||||
if let Some(callback) = platform.0.borrow_mut().event.as_mut() {
|
||||
if callback(event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
|
||||
let platform = get_foreground_platform(this);
|
||||
let callback = platform.0.borrow_mut().finish_launching.take();
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
|
||||
let paths = unsafe {
|
||||
(0..paths.count())
|
||||
.into_iter()
|
||||
.filter_map(|i| {
|
||||
let path = paths.objectAtIndex(i);
|
||||
match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
|
||||
Ok(string) => Some(PathBuf::from(string)),
|
||||
Err(err) => {
|
||||
log::error!("error converting path to string: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() {
|
||||
callback(paths);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let platform = get_foreground_platform(this);
|
||||
let mut platform = platform.0.borrow_mut();
|
||||
if let Some(mut callback) = platform.menu_command.take() {
|
||||
let tag: NSInteger = msg_send![item, tag];
|
||||
let index = tag as usize;
|
||||
if let Some(action) = platform.menu_actions.get(index) {
|
||||
callback(action.as_ref());
|
||||
}
|
||||
platform.menu_command = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn ns_string(string: &str) -> id {
|
||||
NSString::alloc(nil).init_str(string).autorelease()
|
||||
}
|
||||
|
||||
mod security {
|
||||
#![allow(non_upper_case_globals)]
|
||||
use super::*;
|
||||
|
||||
#[link(name = "Security", kind = "framework")]
|
||||
extern "C" {
|
||||
pub static kSecClass: CFStringRef;
|
||||
pub static kSecClassInternetPassword: CFStringRef;
|
||||
pub static kSecAttrServer: CFStringRef;
|
||||
pub static kSecAttrAccount: CFStringRef;
|
||||
pub static kSecValueData: CFStringRef;
|
||||
pub static kSecReturnAttributes: CFStringRef;
|
||||
pub static kSecReturnData: CFStringRef;
|
||||
|
||||
pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
|
||||
pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
|
||||
pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
|
||||
pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
|
||||
}
|
||||
|
||||
pub const errSecSuccess: OSStatus = 0;
|
||||
pub const errSecUserCanceled: OSStatus = -128;
|
||||
pub const errSecItemNotFound: OSStatus = -25300;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::platform::Platform;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clipboard() {
|
||||
let platform = build_platform();
|
||||
assert_eq!(platform.read_from_clipboard(), None);
|
||||
|
||||
let item = ClipboardItem::new("1".to_string());
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let text_from_other_app = "text from other app";
|
||||
unsafe {
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
text_from_other_app.as_ptr() as *const c_void,
|
||||
text_from_other_app.len() as u64,
|
||||
);
|
||||
platform
|
||||
.pasteboard
|
||||
.setData_forType(bytes, NSPasteboardTypeString);
|
||||
}
|
||||
assert_eq!(
|
||||
platform.read_from_clipboard(),
|
||||
Some(ClipboardItem::new(text_from_other_app.to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
fn build_platform() -> MacPlatform {
|
||||
let mut platform = MacPlatform::new();
|
||||
platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||
platform
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,967 @@
|
||||
use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache};
|
||||
use crate::{
|
||||
color::Color,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, vec2i, Vector2F},
|
||||
},
|
||||
platform,
|
||||
scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow},
|
||||
};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
use shaders::ToFloat2 as _;
|
||||
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
|
||||
|
||||
const SHADERS_METALLIB: &'static [u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
|
||||
const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
|
||||
|
||||
pub struct Renderer {
|
||||
sprite_cache: SpriteCache,
|
||||
image_cache: ImageCache,
|
||||
path_atlases: AtlasAllocator,
|
||||
quad_pipeline_state: metal::RenderPipelineState,
|
||||
shadow_pipeline_state: metal::RenderPipelineState,
|
||||
sprite_pipeline_state: metal::RenderPipelineState,
|
||||
image_pipeline_state: metal::RenderPipelineState,
|
||||
path_atlas_pipeline_state: metal::RenderPipelineState,
|
||||
unit_vertices: metal::Buffer,
|
||||
instances: metal::Buffer,
|
||||
}
|
||||
|
||||
struct PathSprite {
|
||||
layer_id: usize,
|
||||
atlas_id: usize,
|
||||
shader_data: shaders::GPUISprite,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
pixel_format: metal::MTLPixelFormat,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
let library = device
|
||||
.new_library_with_data(SHADERS_METALLIB)
|
||||
.expect("error building metal library");
|
||||
|
||||
let unit_vertices = [
|
||||
(0., 0.).to_float2(),
|
||||
(1., 0.).to_float2(),
|
||||
(0., 1.).to_float2(),
|
||||
(0., 1.).to_float2(),
|
||||
(1., 0.).to_float2(),
|
||||
(1., 1.).to_float2(),
|
||||
];
|
||||
let unit_vertices = device.new_buffer_with_data(
|
||||
unit_vertices.as_ptr() as *const c_void,
|
||||
(unit_vertices.len() * mem::size_of::<shaders::vector_float2>()) as u64,
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
let instances = device.new_buffer(
|
||||
INSTANCE_BUFFER_SIZE as u64,
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
|
||||
let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
|
||||
let path_atlases =
|
||||
AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
|
||||
let quad_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
"quad",
|
||||
"quad_vertex",
|
||||
"quad_fragment",
|
||||
pixel_format,
|
||||
);
|
||||
let shadow_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
"shadow",
|
||||
"shadow_vertex",
|
||||
"shadow_fragment",
|
||||
pixel_format,
|
||||
);
|
||||
let sprite_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
"sprite",
|
||||
"sprite_vertex",
|
||||
"sprite_fragment",
|
||||
pixel_format,
|
||||
);
|
||||
let image_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
"image",
|
||||
"image_vertex",
|
||||
"image_fragment",
|
||||
pixel_format,
|
||||
);
|
||||
let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
|
||||
&device,
|
||||
&library,
|
||||
"path_atlas",
|
||||
"path_atlas_vertex",
|
||||
"path_atlas_fragment",
|
||||
MTLPixelFormat::R8Unorm,
|
||||
);
|
||||
Self {
|
||||
sprite_cache,
|
||||
image_cache,
|
||||
path_atlases,
|
||||
quad_pipeline_state,
|
||||
shadow_pipeline_state,
|
||||
sprite_pipeline_state,
|
||||
image_pipeline_state,
|
||||
path_atlas_pipeline_state,
|
||||
unit_vertices,
|
||||
instances,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
drawable_size: Vector2F,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
output: &metal::TextureRef,
|
||||
) {
|
||||
let mut offset = 0;
|
||||
|
||||
let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
|
||||
self.render_layers(
|
||||
scene,
|
||||
path_sprites,
|
||||
&mut offset,
|
||||
drawable_size,
|
||||
command_buffer,
|
||||
output,
|
||||
);
|
||||
self.instances.did_modify_range(NSRange {
|
||||
location: 0,
|
||||
length: offset as NSUInteger,
|
||||
});
|
||||
self.image_cache.finish_frame();
|
||||
}
|
||||
|
||||
fn render_path_atlases(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
offset: &mut usize,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
) -> Vec<PathSprite> {
|
||||
self.path_atlases.clear();
|
||||
let mut sprites = Vec::new();
|
||||
let mut vertices = Vec::<shaders::GPUIPathVertex>::new();
|
||||
let mut current_atlas_id = None;
|
||||
for (layer_id, layer) in scene.layers().enumerate() {
|
||||
for path in layer.paths() {
|
||||
let origin = path.bounds.origin() * scene.scale_factor();
|
||||
let size = (path.bounds.size() * scene.scale_factor()).ceil();
|
||||
let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
|
||||
let atlas_origin = atlas_origin.to_f32();
|
||||
sprites.push(PathSprite {
|
||||
layer_id,
|
||||
atlas_id: alloc_id.atlas_id,
|
||||
shader_data: shaders::GPUISprite {
|
||||
origin: origin.floor().to_float2(),
|
||||
target_size: size.to_float2(),
|
||||
source_size: size.to_float2(),
|
||||
atlas_origin: atlas_origin.to_float2(),
|
||||
color: path.color.to_uchar4(),
|
||||
compute_winding: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if let Some(current_atlas_id) = current_atlas_id {
|
||||
if alloc_id.atlas_id != current_atlas_id {
|
||||
self.render_paths_to_atlas(
|
||||
offset,
|
||||
&vertices,
|
||||
current_atlas_id,
|
||||
command_buffer,
|
||||
);
|
||||
vertices.clear();
|
||||
}
|
||||
}
|
||||
|
||||
current_atlas_id = Some(alloc_id.atlas_id);
|
||||
|
||||
for vertex in &path.vertices {
|
||||
let xy_position =
|
||||
(vertex.xy_position - path.bounds.origin()) * scene.scale_factor();
|
||||
vertices.push(shaders::GPUIPathVertex {
|
||||
xy_position: (atlas_origin + xy_position).to_float2(),
|
||||
st_position: vertex.st_position.to_float2(),
|
||||
clip_rect_origin: atlas_origin.to_float2(),
|
||||
clip_rect_size: size.to_float2(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(atlas_id) = current_atlas_id {
|
||||
self.render_paths_to_atlas(offset, &vertices, atlas_id, command_buffer);
|
||||
}
|
||||
|
||||
sprites
|
||||
}
|
||||
|
||||
fn render_paths_to_atlas(
|
||||
&mut self,
|
||||
offset: &mut usize,
|
||||
vertices: &[shaders::GPUIPathVertex],
|
||||
atlas_id: usize,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
) {
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + vertices.len() * mem::size_of::<shaders::GPUIPathVertex>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
|
||||
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
||||
let color_attachment = render_pass_descriptor
|
||||
.color_attachments()
|
||||
.object_at(0)
|
||||
.unwrap();
|
||||
let texture = self.path_atlases.texture(atlas_id).unwrap();
|
||||
color_attachment.set_texture(Some(texture));
|
||||
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
|
||||
|
||||
let path_atlas_command_encoder =
|
||||
command_buffer.new_render_command_encoder(render_pass_descriptor);
|
||||
path_atlas_command_encoder.set_render_pipeline_state(&self.path_atlas_pipeline_state);
|
||||
path_atlas_command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIPathAtlasVertexInputIndex_GPUIPathAtlasVertexInputIndexVertices as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
path_atlas_command_encoder.set_vertex_bytes(
|
||||
shaders::GPUIPathAtlasVertexInputIndex_GPUIPathAtlasVertexInputIndexAtlasSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
|
||||
as *const c_void,
|
||||
);
|
||||
|
||||
let buffer_contents = unsafe {
|
||||
(self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIPathVertex
|
||||
};
|
||||
|
||||
for (ix, vertex) in vertices.iter().enumerate() {
|
||||
unsafe {
|
||||
*buffer_contents.add(ix) = *vertex;
|
||||
}
|
||||
}
|
||||
|
||||
path_atlas_command_encoder.draw_primitives(
|
||||
metal::MTLPrimitiveType::Triangle,
|
||||
0,
|
||||
vertices.len() as u64,
|
||||
);
|
||||
path_atlas_command_encoder.end_encoding();
|
||||
*offset = next_offset;
|
||||
}
|
||||
|
||||
fn render_layers(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
path_sprites: Vec<PathSprite>,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
output: &metal::TextureRef,
|
||||
) {
|
||||
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
||||
let color_attachment = render_pass_descriptor
|
||||
.color_attachments()
|
||||
.object_at(0)
|
||||
.unwrap();
|
||||
color_attachment.set_texture(Some(output));
|
||||
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
|
||||
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
|
||||
|
||||
command_encoder.set_viewport(metal::MTLViewport {
|
||||
originX: 0.0,
|
||||
originY: 0.0,
|
||||
width: drawable_size.x() as f64,
|
||||
height: drawable_size.y() as f64,
|
||||
znear: 0.0,
|
||||
zfar: 1.0,
|
||||
});
|
||||
|
||||
let scale_factor = scene.scale_factor();
|
||||
let mut path_sprites = path_sprites.into_iter().peekable();
|
||||
for (layer_id, layer) in scene.layers().enumerate() {
|
||||
self.clip(scene, layer, drawable_size, command_encoder);
|
||||
self.render_shadows(
|
||||
layer.shadows(),
|
||||
scale_factor,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
self.render_quads(
|
||||
layer.quads(),
|
||||
scale_factor,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
self.render_path_sprites(
|
||||
layer_id,
|
||||
&mut path_sprites,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
self.render_sprites(
|
||||
layer.glyphs(),
|
||||
layer.icons(),
|
||||
scale_factor,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
self.render_images(
|
||||
layer.images(),
|
||||
scale_factor,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
self.render_quads(
|
||||
layer.underlines(),
|
||||
scale_factor,
|
||||
offset,
|
||||
drawable_size,
|
||||
command_encoder,
|
||||
);
|
||||
}
|
||||
|
||||
command_encoder.end_encoding();
|
||||
}
|
||||
|
||||
fn clip(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
layer: &Layer,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
let clip_bounds = (layer.clip_bounds().unwrap_or(RectF::new(
|
||||
vec2f(0., 0.),
|
||||
drawable_size / scene.scale_factor(),
|
||||
)) * scene.scale_factor())
|
||||
.round();
|
||||
command_encoder.set_scissor_rect(metal::MTLScissorRect {
|
||||
x: clip_bounds.origin_x() as NSUInteger,
|
||||
y: clip_bounds.origin_y() as NSUInteger,
|
||||
width: clip_bounds.width() as NSUInteger,
|
||||
height: clip_bounds.height() as NSUInteger,
|
||||
});
|
||||
}
|
||||
|
||||
fn render_shadows(
|
||||
&mut self,
|
||||
shadows: &[Shadow],
|
||||
scale_factor: f32,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
if shadows.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + shadows.len() * mem::size_of::<shaders::GPUIShadow>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.shadow_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIShadowInputIndex_GPUIShadowInputIndexVertices as u64,
|
||||
Some(&self.unit_vertices),
|
||||
0,
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIShadowInputIndex_GPUIShadowInputIndexShadows as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUIShadowInputIndex_GPUIShadowInputIndexUniforms as u64,
|
||||
mem::size_of::<shaders::GPUIUniforms>() as u64,
|
||||
[shaders::GPUIUniforms {
|
||||
viewport_size: drawable_size.to_float2(),
|
||||
}]
|
||||
.as_ptr() as *const c_void,
|
||||
);
|
||||
|
||||
let buffer_contents = unsafe {
|
||||
(self.instances.contents() as *mut u8).offset(*offset as isize)
|
||||
as *mut shaders::GPUIShadow
|
||||
};
|
||||
for (ix, shadow) in shadows.iter().enumerate() {
|
||||
let shape_bounds = shadow.bounds * scale_factor;
|
||||
let shader_shadow = shaders::GPUIShadow {
|
||||
origin: shape_bounds.origin().to_float2(),
|
||||
size: shape_bounds.size().to_float2(),
|
||||
corner_radius: shadow.corner_radius * scale_factor,
|
||||
sigma: shadow.sigma,
|
||||
color: shadow.color.to_uchar4(),
|
||||
};
|
||||
unsafe {
|
||||
*(buffer_contents.offset(ix as isize)) = shader_shadow;
|
||||
}
|
||||
}
|
||||
|
||||
command_encoder.draw_primitives_instanced(
|
||||
metal::MTLPrimitiveType::Triangle,
|
||||
0,
|
||||
6,
|
||||
shadows.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
}
|
||||
|
||||
fn render_quads(
|
||||
&mut self,
|
||||
quads: &[Quad],
|
||||
scale_factor: f32,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
if quads.is_empty() {
|
||||
return;
|
||||
}
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + quads.len() * mem::size_of::<shaders::GPUIQuad>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.quad_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIQuadInputIndex_GPUIQuadInputIndexVertices as u64,
|
||||
Some(&self.unit_vertices),
|
||||
0,
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIQuadInputIndex_GPUIQuadInputIndexQuads as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUIQuadInputIndex_GPUIQuadInputIndexUniforms as u64,
|
||||
mem::size_of::<shaders::GPUIUniforms>() as u64,
|
||||
[shaders::GPUIUniforms {
|
||||
viewport_size: drawable_size.to_float2(),
|
||||
}]
|
||||
.as_ptr() as *const c_void,
|
||||
);
|
||||
|
||||
let buffer_contents = unsafe {
|
||||
(self.instances.contents() as *mut u8).offset(*offset as isize)
|
||||
as *mut shaders::GPUIQuad
|
||||
};
|
||||
for (ix, quad) in quads.iter().enumerate() {
|
||||
let bounds = quad.bounds * scale_factor;
|
||||
let border_width = quad.border.width * scale_factor;
|
||||
let shader_quad = shaders::GPUIQuad {
|
||||
origin: bounds.origin().round().to_float2(),
|
||||
size: bounds.size().round().to_float2(),
|
||||
background_color: quad
|
||||
.background
|
||||
.unwrap_or(Color::transparent_black())
|
||||
.to_uchar4(),
|
||||
border_top: border_width * (quad.border.top as usize as f32),
|
||||
border_right: border_width * (quad.border.right as usize as f32),
|
||||
border_bottom: border_width * (quad.border.bottom as usize as f32),
|
||||
border_left: border_width * (quad.border.left as usize as f32),
|
||||
border_color: quad.border.color.to_uchar4(),
|
||||
corner_radius: quad.corner_radius * scale_factor,
|
||||
};
|
||||
unsafe {
|
||||
*(buffer_contents.offset(ix as isize)) = shader_quad;
|
||||
}
|
||||
}
|
||||
|
||||
command_encoder.draw_primitives_instanced(
|
||||
metal::MTLPrimitiveType::Triangle,
|
||||
0,
|
||||
6,
|
||||
quads.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
}
|
||||
|
||||
fn render_sprites(
|
||||
&mut self,
|
||||
glyphs: &[Glyph],
|
||||
icons: &[Icon],
|
||||
scale_factor: f32,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
if glyphs.is_empty() && icons.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut sprites_by_atlas = HashMap::new();
|
||||
|
||||
for glyph in glyphs {
|
||||
if let Some(sprite) = self.sprite_cache.render_glyph(
|
||||
glyph.font_id,
|
||||
glyph.font_size,
|
||||
glyph.id,
|
||||
glyph.origin,
|
||||
scale_factor,
|
||||
) {
|
||||
// Snap sprite to pixel grid.
|
||||
let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32();
|
||||
sprites_by_atlas
|
||||
.entry(sprite.atlas_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(shaders::GPUISprite {
|
||||
origin: origin.to_float2(),
|
||||
target_size: sprite.size.to_float2(),
|
||||
source_size: sprite.size.to_float2(),
|
||||
atlas_origin: sprite.atlas_origin.to_float2(),
|
||||
color: glyph.color.to_uchar4(),
|
||||
compute_winding: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for icon in icons {
|
||||
let origin = icon.bounds.origin() * scale_factor;
|
||||
let target_size = icon.bounds.size() * scale_factor;
|
||||
let source_size = (target_size * 2.).ceil().to_i32();
|
||||
|
||||
let sprite =
|
||||
self.sprite_cache
|
||||
.render_icon(source_size, icon.path.clone(), icon.svg.clone());
|
||||
|
||||
sprites_by_atlas
|
||||
.entry(sprite.atlas_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(shaders::GPUISprite {
|
||||
origin: origin.to_float2(),
|
||||
target_size: target_size.to_float2(),
|
||||
source_size: sprite.size.to_float2(),
|
||||
atlas_origin: sprite.atlas_origin.to_float2(),
|
||||
color: icon.color.to_uchar4(),
|
||||
compute_winding: 0,
|
||||
});
|
||||
}
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,
|
||||
Some(&self.unit_vertices),
|
||||
0,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexViewportSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[drawable_size.to_float2()].as_ptr() as *const c_void,
|
||||
);
|
||||
|
||||
for (atlas_id, sprites) in sprites_by_atlas {
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + sprites.len() * mem::size_of::<shaders::GPUISprite>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
|
||||
let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
|
||||
as *const c_void,
|
||||
);
|
||||
|
||||
command_encoder.set_fragment_texture(
|
||||
shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
|
||||
Some(texture),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let buffer_contents = (self.instances.contents() as *mut u8)
|
||||
.offset(*offset as isize)
|
||||
as *mut shaders::GPUISprite;
|
||||
std::ptr::copy_nonoverlapping(sprites.as_ptr(), buffer_contents, sprites.len());
|
||||
}
|
||||
|
||||
command_encoder.draw_primitives_instanced(
|
||||
metal::MTLPrimitiveType::Triangle,
|
||||
0,
|
||||
6,
|
||||
sprites.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_images(
|
||||
&mut self,
|
||||
images: &[Image],
|
||||
scale_factor: f32,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut images_by_atlas = HashMap::new();
|
||||
for image in images {
|
||||
let origin = image.bounds.origin() * scale_factor;
|
||||
let target_size = image.bounds.size() * scale_factor;
|
||||
let corner_radius = image.corner_radius * scale_factor;
|
||||
let border_width = image.border.width * scale_factor;
|
||||
let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
|
||||
images_by_atlas
|
||||
.entry(alloc_id.atlas_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(shaders::GPUIImage {
|
||||
origin: origin.to_float2(),
|
||||
target_size: target_size.to_float2(),
|
||||
source_size: atlas_bounds.size().to_float2(),
|
||||
atlas_origin: atlas_bounds.origin().to_float2(),
|
||||
border_top: border_width * (image.border.top as usize as f32),
|
||||
border_right: border_width * (image.border.right as usize as f32),
|
||||
border_bottom: border_width * (image.border.bottom as usize as f32),
|
||||
border_left: border_width * (image.border.left as usize as f32),
|
||||
border_color: image.border.color.to_uchar4(),
|
||||
corner_radius,
|
||||
});
|
||||
}
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
|
||||
Some(&self.unit_vertices),
|
||||
0,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[drawable_size.to_float2()].as_ptr() as *const c_void,
|
||||
);
|
||||
|
||||
for (atlas_id, images) in images_by_atlas {
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + images.len() * mem::size_of::<shaders::GPUIImage>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
|
||||
let texture = self.image_cache.atlas_texture(atlas_id).unwrap();
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
|
||||
as *const c_void,
|
||||
);
|
||||
command_encoder.set_fragment_texture(
|
||||
shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64,
|
||||
Some(texture),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let buffer_contents = (self.instances.contents() as *mut u8)
|
||||
.offset(*offset as isize)
|
||||
as *mut shaders::GPUIImage;
|
||||
std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len());
|
||||
}
|
||||
|
||||
command_encoder.draw_primitives_instanced(
|
||||
metal::MTLPrimitiveType::Triangle,
|
||||
0,
|
||||
6,
|
||||
images.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_path_sprites(
|
||||
&mut self,
|
||||
layer_id: usize,
|
||||
sprites: &mut Peekable<vec::IntoIter<PathSprite>>,
|
||||
offset: &mut usize,
|
||||
drawable_size: Vector2F,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,
|
||||
Some(&self.unit_vertices),
|
||||
0,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexViewportSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[drawable_size.to_float2()].as_ptr() as *const c_void,
|
||||
);
|
||||
|
||||
let mut atlas_id = None;
|
||||
let mut atlas_sprite_count = 0;
|
||||
align_offset(offset);
|
||||
|
||||
while let Some(sprite) = sprites.peek() {
|
||||
if sprite.layer_id != layer_id {
|
||||
break;
|
||||
}
|
||||
|
||||
let sprite = sprites.next().unwrap();
|
||||
if let Some(atlas_id) = atlas_id.as_mut() {
|
||||
if sprite.atlas_id != *atlas_id {
|
||||
self.render_path_sprites_for_atlas(
|
||||
offset,
|
||||
*atlas_id,
|
||||
atlas_sprite_count,
|
||||
command_encoder,
|
||||
);
|
||||
|
||||
*atlas_id = sprite.atlas_id;
|
||||
atlas_sprite_count = 0;
|
||||
align_offset(offset);
|
||||
}
|
||||
} else {
|
||||
atlas_id = Some(sprite.atlas_id);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let buffer_contents = (self.instances.contents() as *mut u8)
|
||||
.offset(*offset as isize)
|
||||
as *mut shaders::GPUISprite;
|
||||
*buffer_contents.offset(atlas_sprite_count as isize) = sprite.shader_data;
|
||||
}
|
||||
|
||||
atlas_sprite_count += 1;
|
||||
}
|
||||
|
||||
if let Some(atlas_id) = atlas_id {
|
||||
self.render_path_sprites_for_atlas(
|
||||
offset,
|
||||
atlas_id,
|
||||
atlas_sprite_count,
|
||||
command_encoder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_path_sprites_for_atlas(
|
||||
&mut self,
|
||||
offset: &mut usize,
|
||||
atlas_id: usize,
|
||||
sprite_count: usize,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) {
|
||||
let next_offset = *offset + sprite_count * mem::size_of::<shaders::GPUISprite>();
|
||||
assert!(
|
||||
next_offset <= INSTANCE_BUFFER_SIZE,
|
||||
"instance buffer exhausted"
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
);
|
||||
let texture = self.path_atlases.texture(atlas_id).unwrap();
|
||||
command_encoder.set_fragment_texture(
|
||||
shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
|
||||
Some(texture),
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
|
||||
mem::size_of::<shaders::vector_float2>() as u64,
|
||||
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
|
||||
as *const c_void,
|
||||
);
|
||||
|
||||
command_encoder.draw_primitives_instanced(
|
||||
metal::MTLPrimitiveType::Triangle,
|
||||
0,
|
||||
6,
|
||||
sprite_count as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
}
|
||||
}
|
||||
|
||||
fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
|
||||
let texture_descriptor = metal::TextureDescriptor::new();
|
||||
texture_descriptor.set_width(2048);
|
||||
texture_descriptor.set_height(2048);
|
||||
texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm);
|
||||
texture_descriptor
|
||||
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
|
||||
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
|
||||
texture_descriptor
|
||||
}
|
||||
|
||||
fn align_offset(offset: &mut usize) {
|
||||
let r = *offset % 256;
|
||||
if r > 0 {
|
||||
*offset += 256 - r; // Align to a multiple of 256 to make Metal happy
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pipeline_state(
|
||||
device: &metal::DeviceRef,
|
||||
library: &metal::LibraryRef,
|
||||
label: &str,
|
||||
vertex_fn_name: &str,
|
||||
fragment_fn_name: &str,
|
||||
pixel_format: metal::MTLPixelFormat,
|
||||
) -> metal::RenderPipelineState {
|
||||
let vertex_fn = library
|
||||
.get_function(vertex_fn_name, None)
|
||||
.expect("error locating vertex function");
|
||||
let fragment_fn = library
|
||||
.get_function(fragment_fn_name, None)
|
||||
.expect("error locating fragment function");
|
||||
|
||||
let descriptor = metal::RenderPipelineDescriptor::new();
|
||||
descriptor.set_label(label);
|
||||
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
|
||||
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
|
||||
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
|
||||
color_attachment.set_pixel_format(pixel_format);
|
||||
color_attachment.set_blending_enabled(true);
|
||||
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
|
||||
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
|
||||
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
|
||||
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
|
||||
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
|
||||
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
|
||||
|
||||
device
|
||||
.new_render_pipeline_state(&descriptor)
|
||||
.expect("could not create render pipeline state")
|
||||
}
|
||||
|
||||
fn build_path_atlas_pipeline_state(
|
||||
device: &metal::DeviceRef,
|
||||
library: &metal::LibraryRef,
|
||||
label: &str,
|
||||
vertex_fn_name: &str,
|
||||
fragment_fn_name: &str,
|
||||
pixel_format: metal::MTLPixelFormat,
|
||||
) -> metal::RenderPipelineState {
|
||||
let vertex_fn = library
|
||||
.get_function(vertex_fn_name, None)
|
||||
.expect("error locating vertex function");
|
||||
let fragment_fn = library
|
||||
.get_function(fragment_fn_name, None)
|
||||
.expect("error locating fragment function");
|
||||
|
||||
let descriptor = metal::RenderPipelineDescriptor::new();
|
||||
descriptor.set_label(label);
|
||||
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
|
||||
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
|
||||
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
|
||||
color_attachment.set_pixel_format(pixel_format);
|
||||
color_attachment.set_blending_enabled(true);
|
||||
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
|
||||
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
|
||||
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
|
||||
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
|
||||
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One);
|
||||
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
|
||||
|
||||
device
|
||||
.new_render_pipeline_state(&descriptor)
|
||||
.expect("could not create render pipeline state")
|
||||
}
|
||||
|
||||
mod shaders {
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
geometry::vector::{Vector2F, Vector2I},
|
||||
};
|
||||
use std::mem;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
|
||||
|
||||
pub trait ToFloat2 {
|
||||
fn to_float2(&self) -> vector_float2;
|
||||
}
|
||||
|
||||
impl ToFloat2 for (f32, f32) {
|
||||
fn to_float2(&self) -> vector_float2 {
|
||||
unsafe {
|
||||
let mut output = mem::transmute::<_, u32>(self.1.to_bits()) as vector_float2;
|
||||
output <<= 32;
|
||||
output |= mem::transmute::<_, u32>(self.0.to_bits()) as vector_float2;
|
||||
output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFloat2 for Vector2F {
|
||||
fn to_float2(&self) -> vector_float2 {
|
||||
unsafe {
|
||||
let mut output = mem::transmute::<_, u32>(self.y().to_bits()) as vector_float2;
|
||||
output <<= 32;
|
||||
output |= mem::transmute::<_, u32>(self.x().to_bits()) as vector_float2;
|
||||
output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFloat2 for Vector2I {
|
||||
fn to_float2(&self) -> vector_float2 {
|
||||
self.to_f32().to_float2()
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn to_uchar4(&self) -> vector_uchar4 {
|
||||
let mut vec = self.a as vector_uchar4;
|
||||
vec <<= 8;
|
||||
vec |= self.b as vector_uchar4;
|
||||
vec <<= 8;
|
||||
vec |= self.g as vector_uchar4;
|
||||
vec <<= 8;
|
||||
vec |= self.r as vector_uchar4;
|
||||
vec
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#include <simd/simd.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
vector_float2 viewport_size;
|
||||
} GPUIUniforms;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUIQuadInputIndexVertices = 0,
|
||||
GPUIQuadInputIndexQuads = 1,
|
||||
GPUIQuadInputIndexUniforms = 2,
|
||||
} GPUIQuadInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
vector_float2 origin;
|
||||
vector_float2 size;
|
||||
vector_uchar4 background_color;
|
||||
float border_top;
|
||||
float border_right;
|
||||
float border_bottom;
|
||||
float border_left;
|
||||
vector_uchar4 border_color;
|
||||
float corner_radius;
|
||||
} GPUIQuad;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUIShadowInputIndexVertices = 0,
|
||||
GPUIShadowInputIndexShadows = 1,
|
||||
GPUIShadowInputIndexUniforms = 2,
|
||||
} GPUIShadowInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
vector_float2 origin;
|
||||
vector_float2 size;
|
||||
float corner_radius;
|
||||
float sigma;
|
||||
vector_uchar4 color;
|
||||
} GPUIShadow;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUISpriteVertexInputIndexVertices = 0,
|
||||
GPUISpriteVertexInputIndexSprites = 1,
|
||||
GPUISpriteVertexInputIndexViewportSize = 2,
|
||||
GPUISpriteVertexInputIndexAtlasSize = 3,
|
||||
} GPUISpriteVertexInputIndex;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUISpriteFragmentInputIndexAtlas = 0,
|
||||
} GPUISpriteFragmentInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
vector_float2 origin;
|
||||
vector_float2 target_size;
|
||||
vector_float2 source_size;
|
||||
vector_float2 atlas_origin;
|
||||
vector_uchar4 color;
|
||||
uint8_t compute_winding;
|
||||
} GPUISprite;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUIPathAtlasVertexInputIndexVertices = 0,
|
||||
GPUIPathAtlasVertexInputIndexAtlasSize = 1,
|
||||
} GPUIPathAtlasVertexInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
vector_float2 xy_position;
|
||||
vector_float2 st_position;
|
||||
vector_float2 clip_rect_origin;
|
||||
vector_float2 clip_rect_size;
|
||||
} GPUIPathVertex;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUIImageVertexInputIndexVertices = 0,
|
||||
GPUIImageVertexInputIndexImages = 1,
|
||||
GPUIImageVertexInputIndexViewportSize = 2,
|
||||
GPUIImageVertexInputIndexAtlasSize = 3,
|
||||
} GPUIImageVertexInputIndex;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GPUIImageFragmentInputIndexAtlas = 0,
|
||||
} GPUIImageFragmentInputIndex;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
vector_float2 origin;
|
||||
vector_float2 target_size;
|
||||
vector_float2 source_size;
|
||||
vector_float2 atlas_origin;
|
||||
float border_top;
|
||||
float border_right;
|
||||
float border_bottom;
|
||||
float border_left;
|
||||
vector_uchar4 border_color;
|
||||
float corner_radius;
|
||||
} GPUIImage;
|
||||
@@ -0,0 +1,308 @@
|
||||
#include <metal_stdlib>
|
||||
#include "shaders.h"
|
||||
|
||||
using namespace metal;
|
||||
|
||||
float4 coloru_to_colorf(uchar4 coloru) {
|
||||
return float4(coloru) / float4(0xff, 0xff, 0xff, 0xff);
|
||||
}
|
||||
|
||||
float4 to_device_position(float2 pixel_position, float2 viewport_size) {
|
||||
return float4(pixel_position / viewport_size * float2(2., -2.) + float2(-1., 1.), 0., 1.);
|
||||
}
|
||||
|
||||
// A standard gaussian function, used for weighting samples
|
||||
float gaussian(float x, float sigma) {
|
||||
return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma);
|
||||
}
|
||||
|
||||
// This approximates the error function, needed for the gaussian integral
|
||||
float2 erf(float2 x) {
|
||||
float2 s = sign(x);
|
||||
float2 a = abs(x);
|
||||
x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
|
||||
x *= x;
|
||||
return s - s / (x * x);
|
||||
}
|
||||
|
||||
float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize) {
|
||||
float delta = min(halfSize.y - corner - abs(y), 0.);
|
||||
float curved = halfSize.x - corner + sqrt(max(0., corner * corner - delta * delta));
|
||||
float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
|
||||
return integral.y - integral.x;
|
||||
}
|
||||
|
||||
struct QuadFragmentInput {
|
||||
float4 position [[position]];
|
||||
float2 atlas_position; // only used in the image shader
|
||||
float2 origin;
|
||||
float2 size;
|
||||
float4 background_color;
|
||||
float border_top;
|
||||
float border_right;
|
||||
float border_bottom;
|
||||
float border_left;
|
||||
float4 border_color;
|
||||
float corner_radius;
|
||||
};
|
||||
|
||||
float4 quad_sdf(QuadFragmentInput input) {
|
||||
float2 half_size = input.size / 2.;
|
||||
float2 center = input.origin + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float2 rounded_edge_to_point = abs(center_to_point) - half_size + input.corner_radius;
|
||||
float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - input.corner_radius;
|
||||
|
||||
float vertical_border = center_to_point.x <= 0. ? input.border_left : input.border_right;
|
||||
float horizontal_border = center_to_point.y <= 0. ? input.border_top : input.border_bottom;
|
||||
float2 inset_size = half_size - input.corner_radius - float2(vertical_border, horizontal_border);
|
||||
float2 point_to_inset_corner = abs(center_to_point) - inset_size;
|
||||
float border_width;
|
||||
if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
|
||||
border_width = 0.;
|
||||
} else if (point_to_inset_corner.y > point_to_inset_corner.x) {
|
||||
border_width = horizontal_border;
|
||||
} else {
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
float4 color;
|
||||
if (border_width == 0.) {
|
||||
color = input.background_color;
|
||||
} else {
|
||||
float4 border_color = float4(mix(float3(input.background_color), float3(input.border_color), input.border_color.a), 1.);
|
||||
float inset_distance = distance + border_width;
|
||||
color = mix(
|
||||
border_color,
|
||||
input.background_color,
|
||||
saturate(0.5 - inset_distance)
|
||||
);
|
||||
}
|
||||
|
||||
float4 coverage = float4(1., 1., 1., saturate(0.5 - distance));
|
||||
return coverage * color;
|
||||
}
|
||||
|
||||
vertex QuadFragmentInput quad_vertex(
|
||||
uint unit_vertex_id [[vertex_id]],
|
||||
uint quad_id [[instance_id]],
|
||||
constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
|
||||
constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
|
||||
constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
|
||||
) {
|
||||
float2 unit_vertex = unit_vertices[unit_vertex_id];
|
||||
GPUIQuad quad = quads[quad_id];
|
||||
float2 position = unit_vertex * quad.size + quad.origin;
|
||||
float4 device_position = to_device_position(position, uniforms->viewport_size);
|
||||
|
||||
return QuadFragmentInput {
|
||||
device_position,
|
||||
float2(0., 0.),
|
||||
quad.origin,
|
||||
quad.size,
|
||||
coloru_to_colorf(quad.background_color),
|
||||
quad.border_top,
|
||||
quad.border_right,
|
||||
quad.border_bottom,
|
||||
quad.border_left,
|
||||
coloru_to_colorf(quad.border_color),
|
||||
quad.corner_radius,
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 quad_fragment(
|
||||
QuadFragmentInput input [[stage_in]]
|
||||
) {
|
||||
return quad_sdf(input);
|
||||
}
|
||||
|
||||
struct ShadowFragmentInput {
|
||||
float4 position [[position]];
|
||||
vector_float2 origin;
|
||||
vector_float2 size;
|
||||
float corner_radius;
|
||||
float sigma;
|
||||
vector_uchar4 color;
|
||||
};
|
||||
|
||||
vertex ShadowFragmentInput shadow_vertex(
|
||||
uint unit_vertex_id [[vertex_id]],
|
||||
uint shadow_id [[instance_id]],
|
||||
constant float2 *unit_vertices [[buffer(GPUIShadowInputIndexVertices)]],
|
||||
constant GPUIShadow *shadows [[buffer(GPUIShadowInputIndexShadows)]],
|
||||
constant GPUIUniforms *uniforms [[buffer(GPUIShadowInputIndexUniforms)]]
|
||||
) {
|
||||
float2 unit_vertex = unit_vertices[unit_vertex_id];
|
||||
GPUIShadow shadow = shadows[shadow_id];
|
||||
|
||||
float margin = 3. * shadow.sigma;
|
||||
float2 position = unit_vertex * (shadow.size + 2. * margin) + shadow.origin - margin;
|
||||
float4 device_position = to_device_position(position, uniforms->viewport_size);
|
||||
|
||||
return ShadowFragmentInput {
|
||||
device_position,
|
||||
shadow.origin,
|
||||
shadow.size,
|
||||
shadow.corner_radius,
|
||||
shadow.sigma,
|
||||
shadow.color,
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 shadow_fragment(
|
||||
ShadowFragmentInput input [[stage_in]]
|
||||
) {
|
||||
float sigma = input.sigma;
|
||||
float corner_radius = input.corner_radius;
|
||||
float2 half_size = input.size / 2.;
|
||||
float2 center = input.origin + half_size;
|
||||
float2 point = input.position.xy - center;
|
||||
|
||||
// The signal is only non-zero in a limited range, so don't waste samples
|
||||
float low = point.y - half_size.y;
|
||||
float high = point.y + half_size.y;
|
||||
float start = clamp(-3. * sigma, low, high);
|
||||
float end = clamp(3. * sigma, low, high);
|
||||
|
||||
// Accumulate samples (we can get away with surprisingly few samples)
|
||||
float step = (end - start) / 4.;
|
||||
float y = start + step * 0.5;
|
||||
float alpha = 0.;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
alpha += blur_along_x(point.x, point.y - y, sigma, corner_radius, half_size) * gaussian(y, sigma) * step;
|
||||
y += step;
|
||||
}
|
||||
|
||||
return float4(1., 1., 1., alpha) * coloru_to_colorf(input.color);
|
||||
}
|
||||
|
||||
struct SpriteFragmentInput {
|
||||
float4 position [[position]];
|
||||
float2 atlas_position;
|
||||
float4 color [[flat]];
|
||||
uchar compute_winding [[flat]];
|
||||
};
|
||||
|
||||
vertex SpriteFragmentInput sprite_vertex(
|
||||
uint unit_vertex_id [[vertex_id]],
|
||||
uint sprite_id [[instance_id]],
|
||||
constant float2 *unit_vertices [[buffer(GPUISpriteVertexInputIndexVertices)]],
|
||||
constant GPUISprite *sprites [[buffer(GPUISpriteVertexInputIndexSprites)]],
|
||||
constant float2 *viewport_size [[buffer(GPUISpriteVertexInputIndexViewportSize)]],
|
||||
constant float2 *atlas_size [[buffer(GPUISpriteVertexInputIndexAtlasSize)]]
|
||||
) {
|
||||
float2 unit_vertex = unit_vertices[unit_vertex_id];
|
||||
GPUISprite sprite = sprites[sprite_id];
|
||||
float2 position = unit_vertex * sprite.target_size + sprite.origin;
|
||||
float4 device_position = to_device_position(position, *viewport_size);
|
||||
float2 atlas_position = (unit_vertex * sprite.source_size + sprite.atlas_origin) / *atlas_size;
|
||||
|
||||
return SpriteFragmentInput {
|
||||
device_position,
|
||||
atlas_position,
|
||||
coloru_to_colorf(sprite.color),
|
||||
sprite.compute_winding
|
||||
};
|
||||
}
|
||||
|
||||
#define MAX_WINDINGS 32.
|
||||
|
||||
fragment float4 sprite_fragment(
|
||||
SpriteFragmentInput input [[stage_in]],
|
||||
texture2d<float> atlas [[ texture(GPUISpriteFragmentInputIndexAtlas) ]]
|
||||
) {
|
||||
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
|
||||
float4 color = input.color;
|
||||
float4 sample = atlas.sample(atlas_sampler, input.atlas_position);
|
||||
float mask;
|
||||
if (input.compute_winding) {
|
||||
mask = 1. - abs(1. - fmod(sample.r * MAX_WINDINGS, 2.));
|
||||
} else {
|
||||
mask = sample.a;
|
||||
}
|
||||
color.a *= mask;
|
||||
return color;
|
||||
}
|
||||
|
||||
vertex QuadFragmentInput image_vertex(
|
||||
uint unit_vertex_id [[vertex_id]],
|
||||
uint image_id [[instance_id]],
|
||||
constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
|
||||
constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]],
|
||||
constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]],
|
||||
constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]]
|
||||
) {
|
||||
float2 unit_vertex = unit_vertices[unit_vertex_id];
|
||||
GPUIImage image = images[image_id];
|
||||
float2 position = unit_vertex * image.target_size + image.origin;
|
||||
float4 device_position = to_device_position(position, *viewport_size);
|
||||
float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
|
||||
|
||||
return QuadFragmentInput {
|
||||
device_position,
|
||||
atlas_position,
|
||||
image.origin,
|
||||
image.target_size,
|
||||
float4(0.),
|
||||
image.border_top,
|
||||
image.border_right,
|
||||
image.border_bottom,
|
||||
image.border_left,
|
||||
coloru_to_colorf(image.border_color),
|
||||
image.corner_radius,
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 image_fragment(
|
||||
QuadFragmentInput input [[stage_in]],
|
||||
texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
|
||||
) {
|
||||
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
|
||||
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
|
||||
return quad_sdf(input);
|
||||
}
|
||||
|
||||
struct PathAtlasVertexOutput {
|
||||
float4 position [[position]];
|
||||
float2 st_position;
|
||||
float clip_rect_distance [[clip_distance]] [4];
|
||||
};
|
||||
|
||||
struct PathAtlasFragmentInput {
|
||||
float4 position [[position]];
|
||||
float2 st_position;
|
||||
};
|
||||
|
||||
vertex PathAtlasVertexOutput path_atlas_vertex(
|
||||
uint vertex_id [[vertex_id]],
|
||||
constant GPUIPathVertex *vertices [[buffer(GPUIPathAtlasVertexInputIndexVertices)]],
|
||||
constant float2 *atlas_size [[buffer(GPUIPathAtlasVertexInputIndexAtlasSize)]]
|
||||
) {
|
||||
GPUIPathVertex v = vertices[vertex_id];
|
||||
float4 device_position = to_device_position(v.xy_position, *atlas_size);
|
||||
return PathAtlasVertexOutput {
|
||||
device_position,
|
||||
v.st_position,
|
||||
{
|
||||
v.xy_position.x - v.clip_rect_origin.x,
|
||||
v.clip_rect_origin.x + v.clip_rect_size.x - v.xy_position.x,
|
||||
v.xy_position.y - v.clip_rect_origin.y,
|
||||
v.clip_rect_origin.y + v.clip_rect_size.y - v.xy_position.y
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 path_atlas_fragment(
|
||||
PathAtlasFragmentInput input [[stage_in]]
|
||||
) {
|
||||
float2 dx = dfdx(input.st_position);
|
||||
float2 dy = dfdy(input.st_position);
|
||||
float2 gradient = float2(
|
||||
(2. * input.st_position.x) * dx.x - dx.y,
|
||||
(2. * input.st_position.x) * dy.x - dy.y
|
||||
);
|
||||
float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
|
||||
float distance = f / length(gradient);
|
||||
float alpha = saturate(0.5 - distance) / MAX_WINDINGS;
|
||||
return float4(alpha, 0., 0., 1.);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
use super::atlas::AtlasAllocator;
|
||||
use crate::{
|
||||
fonts::{FontId, GlyphId},
|
||||
geometry::vector::{vec2f, Vector2F, Vector2I},
|
||||
platform,
|
||||
};
|
||||
use metal::{MTLPixelFormat, TextureDescriptor};
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{borrow::Cow, collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
struct GlyphDescriptor {
|
||||
font_id: FontId,
|
||||
font_size: OrderedFloat<f32>,
|
||||
glyph_id: GlyphId,
|
||||
subpixel_variant: (u8, u8),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GlyphSprite {
|
||||
pub atlas_id: usize,
|
||||
pub atlas_origin: Vector2I,
|
||||
pub offset: Vector2I,
|
||||
pub size: Vector2I,
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
struct IconDescriptor {
|
||||
path: Cow<'static, str>,
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IconSprite {
|
||||
pub atlas_id: usize,
|
||||
pub atlas_origin: Vector2I,
|
||||
pub size: Vector2I,
|
||||
}
|
||||
|
||||
pub struct SpriteCache {
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
atlases: AtlasAllocator,
|
||||
glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
|
||||
icons: HashMap<IconDescriptor, IconSprite>,
|
||||
}
|
||||
|
||||
impl SpriteCache {
|
||||
pub fn new(
|
||||
device: metal::Device,
|
||||
size: Vector2I,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
let descriptor = TextureDescriptor::new();
|
||||
descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
|
||||
descriptor.set_width(size.x() as u64);
|
||||
descriptor.set_height(size.y() as u64);
|
||||
Self {
|
||||
fonts,
|
||||
atlases: AtlasAllocator::new(device, descriptor),
|
||||
glyphs: Default::default(),
|
||||
icons: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_glyph(
|
||||
&mut self,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
target_position: Vector2F,
|
||||
scale_factor: f32,
|
||||
) -> Option<GlyphSprite> {
|
||||
const SUBPIXEL_VARIANTS: u8 = 4;
|
||||
|
||||
let target_position = target_position * scale_factor;
|
||||
let fonts = &self.fonts;
|
||||
let atlases = &mut self.atlases;
|
||||
let subpixel_variant = (
|
||||
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
|
||||
% SUBPIXEL_VARIANTS,
|
||||
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
|
||||
% SUBPIXEL_VARIANTS,
|
||||
);
|
||||
self.glyphs
|
||||
.entry(GlyphDescriptor {
|
||||
font_id,
|
||||
font_size: OrderedFloat(font_size),
|
||||
glyph_id,
|
||||
subpixel_variant,
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
let subpixel_shift = vec2f(
|
||||
subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
|
||||
subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
|
||||
);
|
||||
let (glyph_bounds, mask) = fonts.rasterize_glyph(
|
||||
font_id,
|
||||
font_size,
|
||||
glyph_id,
|
||||
subpixel_shift,
|
||||
scale_factor,
|
||||
)?;
|
||||
|
||||
let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask);
|
||||
Some(GlyphSprite {
|
||||
atlas_id: alloc_id.atlas_id,
|
||||
atlas_origin: atlas_bounds.origin(),
|
||||
offset: glyph_bounds.origin(),
|
||||
size: glyph_bounds.size(),
|
||||
})
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn render_icon(
|
||||
&mut self,
|
||||
size: Vector2I,
|
||||
path: Cow<'static, str>,
|
||||
svg: usvg::Tree,
|
||||
) -> IconSprite {
|
||||
let atlases = &mut self.atlases;
|
||||
self.icons
|
||||
.entry(IconDescriptor {
|
||||
path,
|
||||
width: size.x(),
|
||||
height: size.y(),
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32).unwrap();
|
||||
resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
|
||||
let mask = pixmap
|
||||
.pixels()
|
||||
.iter()
|
||||
.map(|a| a.alpha())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (alloc_id, atlas_bounds) = atlases.upload(size, &mask);
|
||||
IconSprite {
|
||||
atlas_id: alloc_id.atlas_id,
|
||||
atlas_origin: atlas_bounds.origin(),
|
||||
size,
|
||||
}
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
|
||||
self.atlases.texture(atlas_id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
use crate::{
|
||||
executor,
|
||||
geometry::vector::Vector2F,
|
||||
keymap::Keystroke,
|
||||
platform::{self, Event, WindowContext},
|
||||
Scene,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
|
||||
quartzcore::AutoresizingMask,
|
||||
};
|
||||
use core_graphics::display::CGRect;
|
||||
use ctor::ctor;
|
||||
use foreign_types::ForeignType as _;
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use smol::Timer;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
convert::TryInto,
|
||||
ffi::c_void,
|
||||
mem, ptr,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{geometry::RectFExt, renderer::Renderer};
|
||||
|
||||
const WINDOW_STATE_IVAR: &'static str = "windowState";
|
||||
|
||||
static mut WINDOW_CLASS: *const Class = ptr::null();
|
||||
static mut VIEW_CLASS: *const Class = ptr::null();
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
|
||||
|
||||
#[ctor]
|
||||
unsafe fn build_classes() {
|
||||
WINDOW_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIWindow", class!(NSWindow)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||||
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
|
||||
decl.add_method(
|
||||
sel!(canBecomeMainWindow),
|
||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(canBecomeKeyWindow),
|
||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(sendEvent:),
|
||||
send_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidResize:),
|
||||
window_did_resize as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
|
||||
decl.register()
|
||||
};
|
||||
|
||||
VIEW_CLASS = {
|
||||
let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
|
||||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||||
|
||||
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
|
||||
|
||||
decl.add_method(
|
||||
sel!(keyDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseUp:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseMoved:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseDragged:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(scrollWheel:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.add_method(
|
||||
sel!(makeBackingLayer),
|
||||
make_backing_layer as extern "C" fn(&Object, Sel) -> id,
|
||||
);
|
||||
|
||||
decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
|
||||
decl.add_method(
|
||||
sel!(viewDidChangeBackingProperties),
|
||||
view_did_change_backing_properties as extern "C" fn(&Object, Sel),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(setFrameSize:),
|
||||
set_frame_size as extern "C" fn(&Object, Sel, NSSize),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(displayLayer:),
|
||||
display_layer as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.register()
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Window(Rc<RefCell<WindowState>>);
|
||||
|
||||
struct WindowState {
|
||||
id: usize,
|
||||
native_window: id,
|
||||
event_callback: Option<Box<dyn FnMut(Event)>>,
|
||||
resize_callback: Option<Box<dyn FnMut()>>,
|
||||
close_callback: Option<Box<dyn FnOnce()>>,
|
||||
synthetic_drag_counter: usize,
|
||||
executor: Rc<executor::Foreground>,
|
||||
scene_to_render: Option<Scene>,
|
||||
renderer: Renderer,
|
||||
command_queue: metal::CommandQueue,
|
||||
last_fresh_keydown: Option<(Keystroke, String)>,
|
||||
layer: id,
|
||||
traffic_light_position: Option<Vector2F>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn open(
|
||||
id: usize,
|
||||
options: platform::WindowOptions,
|
||||
executor: Rc<executor::Foreground>,
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
) -> Self {
|
||||
const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
|
||||
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
|
||||
let frame = options.bounds.to_ns_rect();
|
||||
let mut style_mask = NSWindowStyleMask::NSClosableWindowMask
|
||||
| NSWindowStyleMask::NSMiniaturizableWindowMask
|
||||
| NSWindowStyleMask::NSResizableWindowMask
|
||||
| NSWindowStyleMask::NSTitledWindowMask;
|
||||
|
||||
if options.titlebar_appears_transparent {
|
||||
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
|
||||
}
|
||||
|
||||
let native_window: id = msg_send![WINDOW_CLASS, alloc];
|
||||
let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
|
||||
frame,
|
||||
style_mask,
|
||||
NSBackingStoreBuffered,
|
||||
NO,
|
||||
);
|
||||
assert!(!native_window.is_null());
|
||||
|
||||
let device =
|
||||
metal::Device::system_default().expect("could not find default metal device");
|
||||
|
||||
let layer: id = msg_send![class!(CAMetalLayer), layer];
|
||||
let _: () = msg_send![layer, setDevice: device.as_ptr()];
|
||||
let _: () = msg_send![layer, setPixelFormat: PIXEL_FORMAT];
|
||||
let _: () = msg_send![layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
let _: () = msg_send![layer, setPresentsWithTransaction: YES];
|
||||
let _: () = msg_send![
|
||||
layer,
|
||||
setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
|
||||
| AutoresizingMask::HEIGHT_SIZABLE
|
||||
];
|
||||
|
||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||||
let native_view = NSView::init(native_view);
|
||||
assert!(!native_view.is_null());
|
||||
|
||||
let window = Self(Rc::new(RefCell::new(WindowState {
|
||||
id,
|
||||
native_window,
|
||||
event_callback: None,
|
||||
resize_callback: None,
|
||||
close_callback: None,
|
||||
synthetic_drag_counter: 0,
|
||||
executor,
|
||||
scene_to_render: Default::default(),
|
||||
renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts),
|
||||
command_queue: device.new_command_queue(),
|
||||
last_fresh_keydown: None,
|
||||
layer,
|
||||
traffic_light_position: options.traffic_light_position,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
WINDOW_STATE_IVAR,
|
||||
Rc::into_raw(window.0.clone()) as *const c_void,
|
||||
);
|
||||
native_window.setDelegate_(native_window);
|
||||
(*native_view).set_ivar(
|
||||
WINDOW_STATE_IVAR,
|
||||
Rc::into_raw(window.0.clone()) as *const c_void,
|
||||
);
|
||||
|
||||
if let Some(title) = options.title.as_ref() {
|
||||
native_window.setTitle_(NSString::alloc(nil).init_str(title));
|
||||
}
|
||||
if options.titlebar_appears_transparent {
|
||||
native_window.setTitlebarAppearsTransparent_(YES);
|
||||
}
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
|
||||
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
|
||||
native_view.setWantsBestResolutionOpenGLSurface_(YES);
|
||||
|
||||
// From winit crate: On Mojave, views automatically become layer-backed shortly after
|
||||
// being added to a native_window. Changing the layer-backedness of a view breaks the
|
||||
// association between the view and its associated OpenGL context. To work around this,
|
||||
// on we explicitly make the view layer-backed up front so that AppKit doesn't do it
|
||||
// itself and break the association with its context.
|
||||
native_view.setWantsLayer(YES);
|
||||
let _: () = msg_send![
|
||||
native_view,
|
||||
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
|
||||
];
|
||||
|
||||
native_window.setContentView_(native_view.autorelease());
|
||||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
native_window.center();
|
||||
native_window.makeKeyAndOrderFront_(nil);
|
||||
|
||||
window.0.borrow().move_traffic_light();
|
||||
pool.drain();
|
||||
|
||||
window
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_window_id() -> Option<usize> {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let key_window: id = msg_send![app, keyWindow];
|
||||
if msg_send![key_window, isKindOfClass: WINDOW_CLASS] {
|
||||
let id = get_window_state(&*key_window).borrow().id;
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.0.as_ref().borrow().native_window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Window for Window {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn on_event(&mut self, callback: Box<dyn FnMut(Event)>) {
|
||||
self.0.as_ref().borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
|
||||
self.0.as_ref().borrow_mut().resize_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
|
||||
self.0.as_ref().borrow_mut().close_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
level: platform::PromptLevel,
|
||||
msg: &str,
|
||||
answers: &[&str],
|
||||
done_fn: Box<dyn FnOnce(usize)>,
|
||||
) {
|
||||
unsafe {
|
||||
let alert: id = msg_send![class!(NSAlert), alloc];
|
||||
let alert: id = msg_send![alert, init];
|
||||
let alert_style = match level {
|
||||
platform::PromptLevel::Info => 1,
|
||||
platform::PromptLevel::Warning => 0,
|
||||
platform::PromptLevel::Critical => 2,
|
||||
};
|
||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||||
for (ix, answer) in answers.into_iter().enumerate() {
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
}
|
||||
let done_fn = Cell::new(Some(done_fn));
|
||||
let block = ConcreteBlock::new(move |answer: NSInteger| {
|
||||
if let Some(done_fn) = done_fn.take() {
|
||||
(done_fn)(answer.try_into().unwrap());
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![
|
||||
alert,
|
||||
beginSheetModalForWindow: self.0.borrow().native_window
|
||||
completionHandler: block
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::WindowContext for Window {
|
||||
fn size(&self) -> Vector2F {
|
||||
self.0.as_ref().borrow().size()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.0.as_ref().borrow().scale_factor()
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: Scene) {
|
||||
self.0.as_ref().borrow_mut().present_scene(scene);
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
self.0.as_ref().borrow().titlebar_height()
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowState {
|
||||
fn move_traffic_light(&self) {
|
||||
if let Some(traffic_light_position) = self.traffic_light_position {
|
||||
let titlebar_height = self.titlebar_height();
|
||||
|
||||
unsafe {
|
||||
let close_button: id = msg_send![
|
||||
self.native_window,
|
||||
standardWindowButton: NSWindowButton::NSWindowCloseButton
|
||||
];
|
||||
let min_button: id = msg_send![
|
||||
self.native_window,
|
||||
standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
|
||||
];
|
||||
let zoom_button: id = msg_send![
|
||||
self.native_window,
|
||||
standardWindowButton: NSWindowButton::NSWindowZoomButton
|
||||
];
|
||||
|
||||
let mut close_button_frame: CGRect = msg_send![close_button, frame];
|
||||
let mut min_button_frame: CGRect = msg_send![min_button, frame];
|
||||
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
|
||||
let mut origin = vec2f(
|
||||
traffic_light_position.x(),
|
||||
titlebar_height
|
||||
- traffic_light_position.y()
|
||||
- close_button_frame.size.height as f32,
|
||||
);
|
||||
let button_spacing =
|
||||
(min_button_frame.origin.x - close_button_frame.origin.x) as f32;
|
||||
|
||||
close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
|
||||
let _: () = msg_send![close_button, setFrame: close_button_frame];
|
||||
origin.set_x(origin.x() + button_spacing);
|
||||
|
||||
min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
|
||||
let _: () = msg_send![min_button, setFrame: min_button_frame];
|
||||
origin.set_x(origin.x() + button_spacing);
|
||||
|
||||
zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
|
||||
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::WindowContext for WindowState {
|
||||
fn size(&self) -> Vector2F {
|
||||
let NSSize { width, height, .. } =
|
||||
unsafe { NSView::frame(self.native_window.contentView()) }.size;
|
||||
vec2f(width as f32, height as f32)
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
unsafe {
|
||||
let screen: id = msg_send![self.native_window, screen];
|
||||
NSScreen::backingScaleFactor(screen) as f32
|
||||
}
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
unsafe {
|
||||
let frame = NSWindow::frame(self.native_window);
|
||||
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
|
||||
(frame.size.height - content_layout_rect.size.height) as f32
|
||||
}
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: Scene) {
|
||||
self.scene_to_render = Some(scene);
|
||||
unsafe {
|
||||
let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_window_state(object: &Object) -> Rc<RefCell<WindowState>> {
|
||||
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
||||
let rc1 = Rc::from_raw(raw as *mut RefCell<WindowState>);
|
||||
let rc2 = rc1.clone();
|
||||
mem::forget(rc1);
|
||||
rc2
|
||||
}
|
||||
|
||||
unsafe fn drop_window_state(object: &Object) {
|
||||
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
||||
Rc::from_raw(raw as *mut RefCell<WindowState>);
|
||||
}
|
||||
|
||||
extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
|
||||
YES
|
||||
}
|
||||
|
||||
extern "C" fn dealloc_window(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
drop_window_state(this);
|
||||
let () = msg_send![super(this, class!(NSWindow)), dealloc];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn dealloc_view(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
drop_window_state(this);
|
||||
let () = msg_send![super(this, class!(NSView)), dealloc];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let weak_window_state = Rc::downgrade(&window_state);
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
||||
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
|
||||
|
||||
if let Some(event) = event {
|
||||
match &event {
|
||||
Event::LeftMouseDragged { position } => {
|
||||
window_state_borrow.synthetic_drag_counter += 1;
|
||||
window_state_borrow
|
||||
.executor
|
||||
.spawn(synthetic_drag(
|
||||
weak_window_state,
|
||||
window_state_borrow.synthetic_drag_counter,
|
||||
*position,
|
||||
))
|
||||
.detach();
|
||||
}
|
||||
Event::LeftMouseUp { .. } => {
|
||||
window_state_borrow.synthetic_drag_counter += 1;
|
||||
}
|
||||
|
||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||
// were released.
|
||||
Event::KeyDown {
|
||||
chars,
|
||||
keystroke,
|
||||
is_held,
|
||||
} => {
|
||||
let keydown = (keystroke.clone(), chars.clone());
|
||||
if *is_held {
|
||||
if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
window_state_borrow.last_fresh_keydown = Some(keydown);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
||||
drop(window_state_borrow);
|
||||
callback(event);
|
||||
window_state.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
|
||||
unsafe {
|
||||
let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
window_state.as_ref().borrow().move_traffic_light();
|
||||
}
|
||||
|
||||
extern "C" fn close_window(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
let close_callback = {
|
||||
let window_state = get_window_state(this);
|
||||
window_state
|
||||
.as_ref()
|
||||
.try_borrow_mut()
|
||||
.ok()
|
||||
.and_then(|mut window_state| window_state.close_callback.take())
|
||||
};
|
||||
|
||||
if let Some(callback) = close_callback {
|
||||
callback();
|
||||
}
|
||||
|
||||
let () = msg_send![super(this, class!(NSWindow)), close];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let window_state = window_state.as_ref().borrow();
|
||||
window_state.layer
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![window_state_borrow.layer, setContentsScale: window_state_borrow.scale_factor() as f64];
|
||||
}
|
||||
|
||||
if let Some(mut callback) = window_state_borrow.resize_callback.take() {
|
||||
drop(window_state_borrow);
|
||||
callback();
|
||||
window_state.as_ref().borrow_mut().resize_callback = Some(callback);
|
||||
};
|
||||
}
|
||||
|
||||
extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut window_state_borrow = window_state.as_ref().borrow_mut();
|
||||
|
||||
if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
|
||||
}
|
||||
|
||||
let scale_factor = window_state_borrow.scale_factor() as f64;
|
||||
let drawable_size: NSSize = NSSize {
|
||||
width: size.width * scale_factor,
|
||||
height: size.height * scale_factor,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
|
||||
}
|
||||
|
||||
if let Some(mut callback) = window_state_borrow.resize_callback.take() {
|
||||
drop(window_state_borrow);
|
||||
callback();
|
||||
window_state.borrow_mut().resize_callback = Some(callback);
|
||||
};
|
||||
}
|
||||
|
||||
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let window_state = get_window_state(this);
|
||||
let mut window_state = window_state.as_ref().borrow_mut();
|
||||
|
||||
if let Some(scene) = window_state.scene_to_render.take() {
|
||||
let drawable: &metal::MetalDrawableRef = msg_send![window_state.layer, nextDrawable];
|
||||
let command_queue = window_state.command_queue.clone();
|
||||
let command_buffer = command_queue.new_command_buffer();
|
||||
|
||||
let size = window_state.size();
|
||||
let scale_factor = window_state.scale_factor();
|
||||
|
||||
window_state.renderer.render(
|
||||
&scene,
|
||||
size * scale_factor,
|
||||
command_buffer,
|
||||
drawable.texture(),
|
||||
);
|
||||
|
||||
command_buffer.commit();
|
||||
command_buffer.wait_until_completed();
|
||||
drawable.present();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn synthetic_drag(
|
||||
window_state: Weak<RefCell<WindowState>>,
|
||||
drag_id: usize,
|
||||
position: Vector2F,
|
||||
) {
|
||||
loop {
|
||||
Timer::after(Duration::from_millis(16)).await;
|
||||
if let Some(window_state) = window_state.upgrade() {
|
||||
let mut window_state_borrow = window_state.borrow_mut();
|
||||
if window_state_borrow.synthetic_drag_counter == drag_id {
|
||||
if let Some(mut callback) = window_state_borrow.event_callback.take() {
|
||||
drop(window_state_borrow);
|
||||
callback(Event::LeftMouseDragged { position });
|
||||
window_state.borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn ns_string(string: &str) -> id {
|
||||
NSString::alloc(nil).init_str(string).autorelease()
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use super::CursorStyle;
|
||||
use crate::{AnyAction, ClipboardItem};
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
pub struct Platform {
|
||||
dispatcher: Arc<dyn super::Dispatcher>,
|
||||
fonts: Arc<dyn super::FontSystem>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
cursor: Mutex<CursorStyle>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ForegroundPlatform {
|
||||
last_prompt_for_new_path_args: RefCell<Option<(PathBuf, Box<dyn FnOnce(Option<PathBuf>)>)>>,
|
||||
}
|
||||
|
||||
struct Dispatcher;
|
||||
|
||||
pub struct Window {
|
||||
size: Vector2F,
|
||||
scale_factor: f32,
|
||||
current_scene: Option<crate::Scene>,
|
||||
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
|
||||
resize_handlers: Vec<Box<dyn FnMut()>>,
|
||||
close_handlers: Vec<Box<dyn FnOnce()>>,
|
||||
pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
|
||||
}
|
||||
|
||||
impl ForegroundPlatform {
|
||||
pub(crate) fn simulate_new_path_selection(
|
||||
&self,
|
||||
result: impl FnOnce(PathBuf) -> Option<PathBuf>,
|
||||
) {
|
||||
let (dir_path, callback) = self
|
||||
.last_prompt_for_new_path_args
|
||||
.take()
|
||||
.expect("prompt_for_new_path was not called");
|
||||
callback(result(dir_path));
|
||||
}
|
||||
|
||||
pub(crate) fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.last_prompt_for_new_path_args.borrow().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::ForegroundPlatform for ForegroundPlatform {
|
||||
fn on_become_active(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
|
||||
|
||||
fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce() -> ()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn AnyAction)>) {}
|
||||
|
||||
fn set_menus(&self, _: Vec<crate::Menu>) {}
|
||||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_: super::PathPromptOptions,
|
||||
_: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, path: &Path, f: Box<dyn FnOnce(Option<std::path::PathBuf>)>) {
|
||||
*self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), f));
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
dispatcher: Arc::new(Dispatcher),
|
||||
fonts: Arc::new(super::current::FontSystem::new()),
|
||||
current_clipboard_item: Default::default(),
|
||||
cursor: Mutex::new(CursorStyle::Arrow),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Platform for Platform {
|
||||
fn dispatcher(&self) -> Arc<dyn super::Dispatcher> {
|
||||
self.dispatcher.clone()
|
||||
}
|
||||
|
||||
fn fonts(&self) -> std::sync::Arc<dyn super::FontSystem> {
|
||||
self.fonts.clone()
|
||||
}
|
||||
|
||||
fn activate(&self, _ignoring_other_apps: bool) {}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_: usize,
|
||||
options: super::WindowOptions,
|
||||
_executor: Rc<super::executor::Foreground>,
|
||||
) -> Box<dyn super::Window> {
|
||||
Box::new(Window::new(options.bounds.size()))
|
||||
}
|
||||
|
||||
fn key_window_id(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn quit(&self) {}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
*self.current_clipboard_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
}
|
||||
|
||||
fn open_url(&self, _: &str) {}
|
||||
|
||||
fn write_credentials(&self, _: &str, _: &str, _: &[u8]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_credentials(&self, _: &str) -> Result<Option<(String, Vec<u8>)>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, _: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
*self.cursor.lock() = style;
|
||||
}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
fn new(size: Vector2F) -> Self {
|
||||
Self {
|
||||
size,
|
||||
event_handlers: Vec::new(),
|
||||
resize_handlers: Vec::new(),
|
||||
close_handlers: Vec::new(),
|
||||
scale_factor: 1.0,
|
||||
current_scene: None,
|
||||
last_prompt: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Dispatcher for Dispatcher {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run_on_main_thread(&self, task: async_task::Runnable) {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
|
||||
impl super::WindowContext for Window {
|
||||
fn size(&self) -> Vector2F {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.scale_factor
|
||||
}
|
||||
|
||||
fn titlebar_height(&self) -> f32 {
|
||||
24.
|
||||
}
|
||||
|
||||
fn present_scene(&mut self, scene: crate::Scene) {
|
||||
self.current_scene = Some(scene);
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Window for Window {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event)>) {
|
||||
self.event_handlers.push(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
|
||||
self.resize_handlers.push(callback);
|
||||
}
|
||||
|
||||
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
|
||||
self.close_handlers.push(callback);
|
||||
}
|
||||
|
||||
fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box<dyn FnOnce(usize)>) {
|
||||
self.last_prompt.replace(Some(f));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn platform() -> Platform {
|
||||
Platform::new()
|
||||
}
|
||||
|
||||
pub fn foreground_platform() -> ForegroundPlatform {
|
||||
ForegroundPlatform::default()
|
||||
}
|
||||
Reference in New Issue
Block a user