mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-28 09:54:33 +00:00
Blade window transparency (#10973)
Release Notes: - N/A Following up to #10880 TODO: - [x] create window as transparent - [x] X11 - [x] Wayland - [ ] Windows - [x] MacOS (when used with Blade) - [x] enable GPU surface transparency - [x] adjust the pipeline blend modes - [x] adjust shader outputs ![transparency2](https://github.com/zed-industries/zed/assets/107301/d554a41b-5d3f-4420-a857-c64c1747c2d5) Blurred results from @jansol (on Wayland), who contributed to this work: ![zed-blur](https://github.com/zed-industries/zed/assets/107301/a6822171-2dcf-4109-be55-b75557c586de) --------- Co-authored-by: Jan Solanti <jhs@psonet.com>
This commit is contained in:
parent
056c785f4e
commit
e4f13dd561
11 changed files with 343 additions and 127 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -1480,7 +1480,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
|
||||
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
|
@ -1510,7 +1510,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
|
||||
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4604,6 +4604,7 @@ dependencies = [
|
|||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"windows 0.53.0",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
|
@ -11780,6 +11781,19 @@ dependencies = [
|
|||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.2.0"
|
||||
|
|
|
@ -254,8 +254,8 @@ async-recursion = "1.0.0"
|
|||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
|
||||
cap-std = "3.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
|
|
|
@ -111,6 +111,7 @@ wayland-protocols = { version = "0.31.2", features = [
|
|||
"staging",
|
||||
"unstable",
|
||||
] }
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
|
||||
oo7 = "0.3.0"
|
||||
open = "5.1.2"
|
||||
filedescriptor = "0.8.2"
|
||||
|
|
|
@ -28,6 +28,7 @@ pub unsafe fn new_renderer(
|
|||
_native_window: *mut c_void,
|
||||
native_view: *mut c_void,
|
||||
bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
use raw_window_handle as rwh;
|
||||
struct RawWindow {
|
||||
|
@ -64,10 +65,13 @@ pub unsafe fn new_renderer(
|
|||
|
||||
BladeRenderer::new(
|
||||
gpu,
|
||||
gpu::Extent {
|
||||
width: bounds.width as u32,
|
||||
height: bounds.height as u32,
|
||||
depth: 1,
|
||||
BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: bounds.width as u32,
|
||||
height: bounds.height as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -76,7 +80,8 @@ pub unsafe fn new_renderer(
|
|||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
struct GlobalParams {
|
||||
viewport_size: [f32; 2],
|
||||
pad: [u32; 2],
|
||||
premultiplied_alpha: u32,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
//Note: we can't use `Bounds` directly here because
|
||||
|
@ -184,6 +189,10 @@ impl BladePipelines {
|
|||
fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo) -> Self {
|
||||
use gpu::ShaderData as _;
|
||||
|
||||
log::info!(
|
||||
"Initializing Blade pipelines for surface {:?}",
|
||||
surface_info
|
||||
);
|
||||
let shader = gpu.create_shader(gpu::ShaderDesc {
|
||||
source: include_str!("shaders.wgsl"),
|
||||
});
|
||||
|
@ -200,6 +209,18 @@ impl BladePipelines {
|
|||
shader.check_struct_size::<MonochromeSprite>();
|
||||
shader.check_struct_size::<PolychromeSprite>();
|
||||
|
||||
// See https://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha/
|
||||
let blend_mode = match surface_info.alpha {
|
||||
gpu::AlphaMode::Ignored => gpu::BlendState::ALPHA_BLENDING,
|
||||
gpu::AlphaMode::PreMultiplied => gpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
|
||||
gpu::AlphaMode::PostMultiplied => gpu::BlendState::ALPHA_BLENDING,
|
||||
};
|
||||
let color_targets = &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(blend_mode),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}];
|
||||
|
||||
Self {
|
||||
quads: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "quads",
|
||||
|
@ -212,11 +233,7 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_quad"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "shadows",
|
||||
|
@ -229,11 +246,7 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_shadow"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "path_rasterization",
|
||||
|
@ -263,11 +276,7 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_path"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "underlines",
|
||||
|
@ -280,11 +289,7 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_underline"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "mono-sprites",
|
||||
|
@ -297,11 +302,7 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_mono_sprite"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "poly-sprites",
|
||||
|
@ -314,11 +315,7 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_poly_sprite"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
surfaces: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
|
||||
name: "surfaces",
|
||||
|
@ -331,23 +328,25 @@ impl BladePipelines {
|
|||
},
|
||||
depth_stencil: None,
|
||||
fragment: shader.at("fs_surface"),
|
||||
color_targets: &[gpu::ColorTargetState {
|
||||
format: surface_info.format,
|
||||
blend: Some(gpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: gpu::ColorWrites::default(),
|
||||
}],
|
||||
color_targets,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BladeSurfaceConfig {
|
||||
pub size: gpu::Extent,
|
||||
pub transparent: bool,
|
||||
}
|
||||
|
||||
pub struct BladeRenderer {
|
||||
gpu: Arc<gpu::Context>,
|
||||
surface_config: gpu::SurfaceConfig,
|
||||
alpha_mode: gpu::AlphaMode,
|
||||
command_encoder: gpu::CommandEncoder,
|
||||
last_sync_point: Option<gpu::SyncPoint>,
|
||||
pipelines: BladePipelines,
|
||||
instance_belt: BladeBelt,
|
||||
viewport_size: gpu::Extent,
|
||||
path_tiles: HashMap<PathId, AtlasTile>,
|
||||
atlas: Arc<BladeAtlas>,
|
||||
atlas_sampler: gpu::Sampler,
|
||||
|
@ -356,21 +355,19 @@ pub struct BladeRenderer {
|
|||
}
|
||||
|
||||
impl BladeRenderer {
|
||||
fn make_surface_config(size: gpu::Extent) -> gpu::SurfaceConfig {
|
||||
gpu::SurfaceConfig {
|
||||
size,
|
||||
pub fn new(gpu: Arc<gpu::Context>, config: BladeSurfaceConfig) -> Self {
|
||||
let surface_config = gpu::SurfaceConfig {
|
||||
size: config.size,
|
||||
usage: gpu::TextureUsage::TARGET,
|
||||
display_sync: gpu::DisplaySync::Recent,
|
||||
//Note: this matches the original logic of the Metal backend,
|
||||
// but ultimaterly we need to switch to `Linear`.
|
||||
color_space: gpu::ColorSpace::Srgb,
|
||||
allow_exclusive_full_screen: false,
|
||||
transparent: false,
|
||||
}
|
||||
}
|
||||
transparent: config.transparent,
|
||||
};
|
||||
let surface_info = gpu.resize(surface_config);
|
||||
|
||||
pub fn new(gpu: Arc<gpu::Context>, size: gpu::Extent) -> Self {
|
||||
let surface_info = gpu.resize(Self::make_surface_config(size));
|
||||
let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc {
|
||||
name: "main",
|
||||
buffer_count: 2,
|
||||
|
@ -397,11 +394,12 @@ impl BladeRenderer {
|
|||
|
||||
Self {
|
||||
gpu,
|
||||
surface_config,
|
||||
alpha_mode: surface_info.alpha,
|
||||
command_encoder,
|
||||
last_sync_point: None,
|
||||
pipelines,
|
||||
instance_belt,
|
||||
viewport_size: size,
|
||||
path_tiles: HashMap::default(),
|
||||
atlas,
|
||||
atlas_sampler,
|
||||
|
@ -425,15 +423,26 @@ impl BladeRenderer {
|
|||
depth: 1,
|
||||
};
|
||||
|
||||
if gpu_size != self.viewport_size() {
|
||||
if gpu_size != self.surface_config.size {
|
||||
self.wait_for_gpu();
|
||||
self.gpu.resize(Self::make_surface_config(gpu_size));
|
||||
self.viewport_size = gpu_size;
|
||||
self.surface_config.size = gpu_size;
|
||||
self.gpu.resize(self.surface_config);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&mut self, transparent: bool) {
|
||||
if transparent != self.surface_config.transparent {
|
||||
self.wait_for_gpu();
|
||||
self.surface_config.transparent = transparent;
|
||||
let surface_info = self.gpu.resize(self.surface_config);
|
||||
self.pipelines = BladePipelines::new(&self.gpu, surface_info);
|
||||
self.alpha_mode = surface_info.alpha;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||
pub fn viewport_size(&self) -> gpu::Extent {
|
||||
self.viewport_size
|
||||
self.surface_config.size
|
||||
}
|
||||
|
||||
pub fn sprite_atlas(&self) -> &Arc<BladeAtlas> {
|
||||
|
@ -481,7 +490,8 @@ impl BladeRenderer {
|
|||
let tex_info = self.atlas.get_texture_info(texture_id);
|
||||
let globals = GlobalParams {
|
||||
viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32],
|
||||
pad: [0; 2],
|
||||
premultiplied_alpha: 0,
|
||||
pad: 0,
|
||||
};
|
||||
|
||||
let vertex_buf = unsafe { self.instance_belt.alloc_data(&vertices, &self.gpu) };
|
||||
|
@ -526,10 +536,14 @@ impl BladeRenderer {
|
|||
|
||||
let globals = GlobalParams {
|
||||
viewport_size: [
|
||||
self.viewport_size.width as f32,
|
||||
self.viewport_size.height as f32,
|
||||
self.surface_config.size.width as f32,
|
||||
self.surface_config.size.height as f32,
|
||||
],
|
||||
pad: [0; 2],
|
||||
premultiplied_alpha: match self.alpha_mode {
|
||||
gpu::AlphaMode::Ignored | gpu::AlphaMode::PostMultiplied => 0,
|
||||
gpu::AlphaMode::PreMultiplied => 1,
|
||||
},
|
||||
pad: 0,
|
||||
};
|
||||
|
||||
if let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
struct GlobalParams {
|
||||
viewport_size: vec2<f32>,
|
||||
pad: vec2<u32>,
|
||||
premultiplied_alpha: u32,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
var<uniform> globals: GlobalParams;
|
||||
|
@ -176,6 +177,13 @@ fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
|
|||
corner_radius;
|
||||
}
|
||||
|
||||
// Abstract away the final color transformation based on the
|
||||
// target alpha compositing mode.
|
||||
fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
||||
let alpha = color.a * alpha_factor;
|
||||
return select(vec4<f32>(color.rgb, alpha), vec4<f32>(color.rgb, 1.0) * alpha, globals.premultiplied_alpha != 0u);
|
||||
}
|
||||
|
||||
// --- quads --- //
|
||||
|
||||
struct Quad {
|
||||
|
@ -266,7 +274,7 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
|||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
return color * vec4<f32>(1.0, 1.0, 1.0, saturate(0.5 - distance));
|
||||
return blend_color(color, saturate(0.5 - distance));
|
||||
}
|
||||
|
||||
// --- shadows --- //
|
||||
|
@ -339,7 +347,7 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
|
|||
y += step;
|
||||
}
|
||||
|
||||
return input.color * vec4<f32>(1.0, 1.0, 1.0, alpha);
|
||||
return blend_color(input.color, alpha);
|
||||
}
|
||||
|
||||
// --- path rasterization --- //
|
||||
|
@ -415,7 +423,7 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
|||
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
|
||||
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
||||
let mask = 1.0 - abs(1.0 - sample % 2.0);
|
||||
return input.color * mask;
|
||||
return blend_color(input.color, mask);
|
||||
}
|
||||
|
||||
// --- underlines --- //
|
||||
|
@ -476,7 +484,7 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
|
|||
let distance_from_top_border = distance_in_pixels - half_thickness;
|
||||
let distance_from_bottom_border = distance_in_pixels + half_thickness;
|
||||
let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
|
||||
return input.color * vec4<f32>(1.0, 1.0, 1.0, alpha);
|
||||
return blend_color(input.color, alpha);
|
||||
}
|
||||
|
||||
// --- monochrome sprites --- //
|
||||
|
@ -520,7 +528,7 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
|
|||
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
return input.color * vec4<f32>(1.0, 1.0, 1.0, sample);
|
||||
return blend_color(input.color, sample);
|
||||
}
|
||||
|
||||
// --- polychrome sprites --- //
|
||||
|
@ -571,8 +579,7 @@ fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
|
|||
let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
|
||||
color = vec4<f32>(vec3<f32>(grayscale), sample.a);
|
||||
}
|
||||
color.a *= saturate(0.5 - distance);
|
||||
return color;
|
||||
return blend_color(color, saturate(0.5 - distance));
|
||||
}
|
||||
|
||||
// --- surfaces --- //
|
||||
|
|
|
@ -24,7 +24,7 @@ use wayland_client::protocol::wl_callback::{self, WlCallback};
|
|||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource};
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output,
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
};
|
||||
use wayland_client::{
|
||||
delegate_noop,
|
||||
|
@ -47,6 +47,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
|||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
|
@ -82,6 +83,7 @@ pub struct Globals {
|
|||
pub fractional_scale_manager:
|
||||
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
|
||||
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
|
@ -114,6 +116,7 @@ impl Globals {
|
|||
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
executor,
|
||||
qh,
|
||||
}
|
||||
|
@ -557,8 +560,11 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDevic
|
|||
delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ use collections::{HashMap, HashSet};
|
|||
use futures::channel::oneshot::Receiver;
|
||||
use raw_window_handle as rwh;
|
||||
use wayland_backend::client::ObjectId;
|
||||
use wayland_client::protocol::wl_region::WlRegion;
|
||||
use wayland_client::WEnum;
|
||||
use wayland_client::{protocol::wl_surface, Proxy};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
|
||||
|
@ -18,8 +19,9 @@ use wayland_protocols::wp::viewporter::client::wp_viewport;
|
|||
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
|
||||
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
|
||||
use crate::platform::blade::BladeRenderer;
|
||||
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
|
@ -67,6 +69,7 @@ pub struct WaylandWindowState {
|
|||
acknowledged_first_configure: bool,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashSet<ObjectId>,
|
||||
|
@ -124,10 +127,13 @@ impl WaylandWindowState {
|
|||
}
|
||||
.unwrap(),
|
||||
);
|
||||
let extent = gpu::Extent {
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
depth: 1,
|
||||
let config = BladeSurfaceConfig {
|
||||
size: gpu::Extent {
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
depth: 1,
|
||||
},
|
||||
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
|
||||
};
|
||||
|
||||
Self {
|
||||
|
@ -135,13 +141,12 @@ impl WaylandWindowState {
|
|||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
blur: None,
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
|
||||
outputs: HashSet::default(),
|
||||
|
||||
renderer: BladeRenderer::new(gpu, extent),
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
|
@ -166,6 +171,9 @@ impl Drop for WaylandWindow {
|
|||
if let Some(decoration) = &state.decoration {
|
||||
decoration.destroy();
|
||||
}
|
||||
if let Some(blur) = &state.blur {
|
||||
blur.release();
|
||||
}
|
||||
state.toplevel.destroy();
|
||||
if let Some(viewport) = &state.viewport {
|
||||
viewport.destroy();
|
||||
|
@ -615,8 +623,44 @@ impl PlatformWindow for WaylandWindow {
|
|||
self.borrow_mut().toplevel.set_app_id(app_id.to_owned());
|
||||
}
|
||||
|
||||
fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
|
||||
// todo(linux)
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
|
||||
let mut state = self.borrow_mut();
|
||||
state.renderer.update_transparency(!opaque);
|
||||
|
||||
let region = state
|
||||
.globals
|
||||
.compositor
|
||||
.create_region(&state.globals.qh, ());
|
||||
region.add(0, 0, i32::MAX, i32::MAX);
|
||||
|
||||
if opaque {
|
||||
// Promise the compositor that this region of the window surface
|
||||
// contains no transparent pixels. This allows the compositor to
|
||||
// do skip whatever is behind the surface for better performance.
|
||||
state.surface.set_opaque_region(Some(®ion));
|
||||
} else {
|
||||
state.surface.set_opaque_region(None);
|
||||
}
|
||||
|
||||
if let Some(ref blur_manager) = state.globals.blur_manager {
|
||||
if (background_appearance == WindowBackgroundAppearance::Blurred) {
|
||||
if (state.blur.is_none()) {
|
||||
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||
blur.set_region(Some(®ion));
|
||||
state.blur = Some(blur);
|
||||
}
|
||||
state.blur.as_ref().unwrap().commit();
|
||||
} else {
|
||||
// It probably doesn't hurt to clear the blur for opaque windows
|
||||
blur_manager.unset(&state.surface);
|
||||
if let Some(b) = state.blur.take() {
|
||||
b.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
region.destroy();
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
|
|
|
@ -2,20 +2,22 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
platform::blade::BladeRenderer, size, Bounds, DevicePixels, ForegroundExecutor, Modifiers,
|
||||
Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowOptions, WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams,
|
||||
X11Client, X11ClientState, X11ClientStatePtr,
|
||||
};
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
use util::ResultExt;
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
connection::{Connection as _, RequestConnection as _},
|
||||
protocol::{
|
||||
render::{self, ConnectionExt as _},
|
||||
xinput,
|
||||
xproto::{self, ConnectionExt as _, CreateWindowAux},
|
||||
xproto::{self, ConnectionExt as _},
|
||||
},
|
||||
resource_manager::Database,
|
||||
wrapper::ConnectionExt,
|
||||
|
@ -24,6 +26,7 @@ use x11rb::{
|
|||
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
iter::Zip,
|
||||
mem,
|
||||
|
@ -61,6 +64,76 @@ fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window)
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Visual {
|
||||
id: xproto::Visualid,
|
||||
colormap: u32,
|
||||
depth: u8,
|
||||
}
|
||||
|
||||
struct VisualSet {
|
||||
inherit: Visual,
|
||||
opaque: Option<Visual>,
|
||||
transparent: Option<Visual>,
|
||||
root: u32,
|
||||
black_pixel: u32,
|
||||
}
|
||||
|
||||
fn find_visuals(xcb_connection: &XCBConnection, screen_index: usize) -> VisualSet {
|
||||
let screen = &xcb_connection.setup().roots[screen_index];
|
||||
let mut set = VisualSet {
|
||||
inherit: Visual {
|
||||
id: screen.root_visual,
|
||||
colormap: screen.default_colormap,
|
||||
depth: screen.root_depth,
|
||||
},
|
||||
opaque: None,
|
||||
transparent: None,
|
||||
root: screen.root,
|
||||
black_pixel: screen.black_pixel,
|
||||
};
|
||||
|
||||
for depth_info in screen.allowed_depths.iter() {
|
||||
for visual_type in depth_info.visuals.iter() {
|
||||
let visual = Visual {
|
||||
id: visual_type.visual_id,
|
||||
colormap: 0,
|
||||
depth: depth_info.depth,
|
||||
};
|
||||
log::debug!("Visual id: {}, class: {:?}, depth: {}, bits_per_value: {}, masks: 0x{:x} 0x{:x} 0x{:x}",
|
||||
visual_type.visual_id,
|
||||
visual_type.class,
|
||||
depth_info.depth,
|
||||
visual_type.bits_per_rgb_value,
|
||||
visual_type.red_mask, visual_type.green_mask, visual_type.blue_mask,
|
||||
);
|
||||
|
||||
if (
|
||||
visual_type.red_mask,
|
||||
visual_type.green_mask,
|
||||
visual_type.blue_mask,
|
||||
) != (0xFF0000, 0xFF00, 0xFF)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let color_mask = visual_type.red_mask | visual_type.green_mask | visual_type.blue_mask;
|
||||
let alpha_mask = color_mask as usize ^ ((1usize << depth_info.depth) - 1);
|
||||
|
||||
if alpha_mask == 0 {
|
||||
if set.opaque.is_none() {
|
||||
set.opaque = Some(visual);
|
||||
}
|
||||
} else {
|
||||
if set.transparent.is_none() {
|
||||
set.transparent = Some(visual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
connection: *mut c_void,
|
||||
screen_id: usize,
|
||||
|
@ -90,7 +163,6 @@ pub(crate) struct X11WindowState {
|
|||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
}
|
||||
|
||||
|
@ -106,7 +178,8 @@ pub(crate) struct X11WindowStatePtr {
|
|||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||
let non_zero = NonZeroU32::new(self.window_id).unwrap();
|
||||
let handle = rwh::XcbWindowHandle::new(non_zero);
|
||||
let mut handle = rwh::XcbWindowHandle::new(non_zero);
|
||||
handle.visual_id = NonZeroU32::new(self.visual_id);
|
||||
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
|
@ -144,39 +217,77 @@ impl X11WindowState {
|
|||
let x_screen_index = params
|
||||
.display_id
|
||||
.map_or(x_main_screen_index, |did| did.0 as usize);
|
||||
let screen = xcb_connection.setup().roots.get(x_screen_index).unwrap();
|
||||
|
||||
let win_aux = xproto::CreateWindowAux::new().event_mask(
|
||||
xproto::EventMask::EXPOSURE
|
||||
| xproto::EventMask::STRUCTURE_NOTIFY
|
||||
| xproto::EventMask::ENTER_WINDOW
|
||||
| xproto::EventMask::LEAVE_WINDOW
|
||||
| xproto::EventMask::FOCUS_CHANGE
|
||||
| xproto::EventMask::KEY_PRESS
|
||||
| xproto::EventMask::KEY_RELEASE
|
||||
| xproto::EventMask::BUTTON_PRESS
|
||||
| xproto::EventMask::BUTTON_RELEASE
|
||||
| xproto::EventMask::POINTER_MOTION
|
||||
| xproto::EventMask::BUTTON1_MOTION
|
||||
| xproto::EventMask::BUTTON2_MOTION
|
||||
| xproto::EventMask::BUTTON3_MOTION
|
||||
| xproto::EventMask::BUTTON_MOTION,
|
||||
);
|
||||
let visual_set = find_visuals(&xcb_connection, x_screen_index);
|
||||
let visual_maybe = match params.window_background {
|
||||
WindowBackgroundAppearance::Opaque => visual_set.opaque,
|
||||
WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => {
|
||||
visual_set.transparent
|
||||
}
|
||||
};
|
||||
let visual = match visual_maybe {
|
||||
Some(visual) => visual,
|
||||
None => {
|
||||
log::warn!(
|
||||
"Unable to find a matching visual for {:?}",
|
||||
params.window_background
|
||||
);
|
||||
visual_set.inherit
|
||||
}
|
||||
};
|
||||
log::info!("Using {:?}", visual);
|
||||
|
||||
let colormap = if visual.colormap != 0 {
|
||||
visual.colormap
|
||||
} else {
|
||||
let id = xcb_connection.generate_id().unwrap();
|
||||
log::info!("Creating colormap {}", id);
|
||||
xcb_connection
|
||||
.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
id
|
||||
};
|
||||
|
||||
let win_aux = xproto::CreateWindowAux::new()
|
||||
.background_pixel(x11rb::NONE)
|
||||
// https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh
|
||||
.border_pixel(visual_set.black_pixel)
|
||||
.colormap(colormap)
|
||||
.event_mask(
|
||||
xproto::EventMask::EXPOSURE
|
||||
| xproto::EventMask::STRUCTURE_NOTIFY
|
||||
| xproto::EventMask::ENTER_WINDOW
|
||||
| xproto::EventMask::LEAVE_WINDOW
|
||||
| xproto::EventMask::FOCUS_CHANGE
|
||||
| xproto::EventMask::KEY_PRESS
|
||||
| xproto::EventMask::KEY_RELEASE
|
||||
| xproto::EventMask::BUTTON_PRESS
|
||||
| xproto::EventMask::BUTTON_RELEASE
|
||||
| xproto::EventMask::POINTER_MOTION
|
||||
| xproto::EventMask::BUTTON1_MOTION
|
||||
| xproto::EventMask::BUTTON2_MOTION
|
||||
| xproto::EventMask::BUTTON3_MOTION
|
||||
| xproto::EventMask::BUTTON_MOTION,
|
||||
);
|
||||
|
||||
xcb_connection
|
||||
.create_window(
|
||||
x11rb::COPY_FROM_PARENT as _,
|
||||
visual.depth,
|
||||
x_window,
|
||||
screen.root,
|
||||
visual_set.root,
|
||||
params.bounds.origin.x.0 as i16,
|
||||
params.bounds.origin.y.0 as i16,
|
||||
params.bounds.size.width.0 as u16,
|
||||
params.bounds.size.height.0 as u16,
|
||||
0,
|
||||
xproto::WindowClass::INPUT_OUTPUT,
|
||||
screen.root_visual,
|
||||
visual.id,
|
||||
&win_aux,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
|
||||
xinput::ConnectionExt::xinput_xi_select_events(
|
||||
|
@ -224,7 +335,7 @@ impl X11WindowState {
|
|||
) as *mut _,
|
||||
screen_id: x_screen_index,
|
||||
window_id: x_window,
|
||||
visual_id: screen.root_visual,
|
||||
visual_id: visual.id,
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
|
@ -240,9 +351,12 @@ impl X11WindowState {
|
|||
.unwrap(),
|
||||
);
|
||||
|
||||
// Note: this has to be done after the GPU init, or otherwise
|
||||
// the sizes are immediately invalidated.
|
||||
let gpu_extent = query_render_extent(xcb_connection, x_window);
|
||||
let config = BladeSurfaceConfig {
|
||||
// Note: this has to be done after the GPU init, or otherwise
|
||||
// the sizes are immediately invalidated.
|
||||
size: query_render_extent(xcb_connection, x_window),
|
||||
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
|
||||
};
|
||||
|
||||
Self {
|
||||
client,
|
||||
|
@ -251,9 +365,8 @@ impl X11WindowState {
|
|||
raw,
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor,
|
||||
renderer: BladeRenderer::new(gpu, gpu_extent),
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
atoms: *atoms,
|
||||
|
||||
input_handler: None,
|
||||
}
|
||||
}
|
||||
|
@ -533,8 +646,10 @@ impl PlatformWindow for X11Window {
|
|||
// todo(linux)
|
||||
fn set_edited(&mut self, edited: bool) {}
|
||||
|
||||
fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
|
||||
// todo(linux)
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
let transparent = background_appearance != WindowBackgroundAppearance::Opaque;
|
||||
inner.renderer.update_transparency(transparent);
|
||||
}
|
||||
|
||||
// todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS,
|
||||
|
|
|
@ -37,6 +37,7 @@ pub unsafe fn new_renderer(
|
|||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context)
|
||||
}
|
||||
|
@ -231,6 +232,10 @@ impl MetalRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&mut self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
}
|
||||
|
||||
pub fn destroy(&mut self) {
|
||||
// nothing to do
|
||||
}
|
||||
|
|
|
@ -626,6 +626,7 @@ impl MacWindow {
|
|||
native_window as *mut _,
|
||||
native_view as *mut _,
|
||||
window_size,
|
||||
window_background != WindowBackgroundAppearance::Opaque,
|
||||
),
|
||||
request_frame_callback: None,
|
||||
event_callback: None,
|
||||
|
@ -979,7 +980,10 @@ impl PlatformWindow for MacWindow {
|
|||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let this = self.0.as_ref().lock();
|
||||
let mut this = self.0.as_ref().lock();
|
||||
this.renderer
|
||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
||||
|
||||
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
80
|
||||
} else {
|
||||
|
|
|
@ -35,7 +35,7 @@ use windows::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::platform::blade::BladeRenderer;
|
||||
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct WindowsWindowInner {
|
||||
|
@ -62,6 +62,7 @@ impl WindowsWindowInner {
|
|||
handle: AnyWindowHandle,
|
||||
hide_title_bar: bool,
|
||||
display: Rc<WindowsDisplay>,
|
||||
transparent: bool,
|
||||
) -> Self {
|
||||
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
|
||||
let origin = Cell::new(Point {
|
||||
|
@ -95,7 +96,7 @@ impl WindowsWindowInner {
|
|||
}
|
||||
}
|
||||
|
||||
let raw = RawWindow { hwnd: hwnd.0 as _ };
|
||||
let raw = RawWindow { hwnd: hwnd.0 };
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init_windowed(
|
||||
|
@ -109,12 +110,11 @@ impl WindowsWindowInner {
|
|||
}
|
||||
.unwrap(),
|
||||
);
|
||||
let extent = gpu::Extent {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth: 1,
|
||||
let config = BladeSurfaceConfig {
|
||||
size: gpu::Extent::default(),
|
||||
transparent,
|
||||
};
|
||||
let renderer = RefCell::new(BladeRenderer::new(gpu, extent));
|
||||
let renderer = RefCell::new(BladeRenderer::new(gpu, config));
|
||||
let callbacks = RefCell::new(Callbacks::default());
|
||||
let display = RefCell::new(display);
|
||||
let click_state = RefCell::new(ClickState::new());
|
||||
|
@ -1241,6 +1241,7 @@ struct WindowCreateContext {
|
|||
handle: AnyWindowHandle,
|
||||
hide_title_bar: bool,
|
||||
display: Rc<WindowsDisplay>,
|
||||
transparent: bool,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
|
@ -1279,6 +1280,7 @@ impl WindowsWindow {
|
|||
// todo(windows) move window to target monitor
|
||||
// options.display_id
|
||||
display: Rc::new(WindowsDisplay::primary_monitor().unwrap()),
|
||||
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
|
||||
};
|
||||
let lpparam = Some(&context as *const _ as *const _);
|
||||
unsafe {
|
||||
|
@ -1511,8 +1513,11 @@ impl PlatformWindow for WindowsWindow {
|
|||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
|
||||
// todo(windows)
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
self.inner
|
||||
.renderer
|
||||
.borrow_mut()
|
||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
|
@ -1783,6 +1788,7 @@ unsafe extern "system" fn wnd_proc(
|
|||
ctx.handle,
|
||||
ctx.hide_title_bar,
|
||||
ctx.display.clone(),
|
||||
ctx.transparent,
|
||||
));
|
||||
let weak = Box::new(Rc::downgrade(&inner));
|
||||
unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
|
||||
|
|
Loading…
Reference in a new issue