use std::fmt::Debug;
use std::sync::Arc;
use iced::{Color, Event, Point, Size, Task};
use iced_wgpu::wgpu;
use truce_core::editor::{Editor, PluginContext};
use truce_gui::EditorScale;
use truce_gui::layout::GridLayout;
use truce_params::Params;
use crate::auto_layout;
use crate::param_cache::ParamCache;
use crate::param_message::{Message, ParamMessage};
pub trait IcedPlugin<P: Params>: Sized + 'static {
type Message: Debug + Clone + Send;
fn new(params: Arc<P>) -> Self;
fn update(
&mut self,
_message: Message<Self::Message>,
_params: &ParamCache<P>,
_ctx: &PluginContext<P>,
) -> Task<Message<Self::Message>> {
Task::none()
}
fn view<'a>(&'a self, params: &'a ParamCache<P>) -> iced::Element<'a, Message<Self::Message>>;
fn theme(&self) -> iced::Theme {
crate::theme::truce_dark_theme()
}
fn title(&self) -> String {
String::from("Plugin")
}
fn state_changed(&mut self) {}
}
pub struct AutoPlugin {
layout: GridLayout,
}
impl<P: Params> IcedPlugin<P> for AutoPlugin {
type Message = ();
fn new(_params: Arc<P>) -> Self {
panic!("AutoPlugin must be created via IcedEditor::from_layout");
}
fn view<'a>(&'a self, params: &'a ParamCache<P>) -> iced::Element<'a, Message<()>> {
auto_layout::auto_view(&self.layout, params)
}
}
pub(crate) struct IcedProgram<P: Params + 'static, M: IcedPlugin<P>> {
pub(crate) plugin: M,
pub(crate) param_cache: ParamCache<P>,
pub(crate) context: PluginContext<P>,
pub(crate) meter_ids: Vec<u32>,
}
impl<P: Params + 'static, M: IcedPlugin<P>> IcedProgram<P, M> {
fn apply_param_message(&self, msg: &ParamMessage) {
match msg {
ParamMessage::BeginEdit(id) => self.context.begin_edit(*id),
ParamMessage::SetNormalized(id, val) => self.context.set_param(*id, *val),
ParamMessage::EndEdit(id) => self.context.end_edit(*id),
ParamMessage::Batch(msgs) => {
for m in msgs {
self.apply_param_message(m);
}
}
}
}
pub(crate) fn dispatch(&mut self, message: Message<M::Message>) {
if let Message::Param(ref param_msg) = message {
self.apply_param_message(param_msg);
}
match message {
Message::Tick => {
self.param_cache.sync(&self.context);
self.param_cache.sync_meters(&self.context, &self.meter_ids);
}
other => {
let _: Task<Message<M::Message>> =
self.plugin.update(other, &self.param_cache, &self.context);
}
}
}
pub(crate) fn view(&self) -> iced::Element<'_, Message<M::Message>> {
self.plugin.view(&self.param_cache)
}
}
pub struct IcedEditor<P, M>
where
P: Params + 'static,
M: IcedPlugin<P>,
{
params: Arc<P>,
size: (u32, u32),
scale: EditorScale,
font: Option<&'static [u8]>,
runtime: Option<IcedRuntime<P, M>>,
make_plugin: Box<dyn Fn(Arc<P>) -> M + Send + Sync>,
meter_ids: Vec<u32>,
baseview_window: Option<baseview::WindowHandle>,
}
unsafe impl<P: Params, M: IcedPlugin<P>> Send for IcedEditor<P, M> {}
impl<P: Params + 'static, M: IcedPlugin<P> + 'static> Drop for IcedEditor<P, M> {
fn drop(&mut self) {
Editor::close(self);
}
}
impl<P: Params + 'static> IcedEditor<P, AutoPlugin> {
pub fn from_layout(params: Arc<P>, layout: GridLayout) -> Self {
let size = (layout.width, layout.height);
let meter_ids: Vec<u32> = layout
.widgets
.iter()
.filter_map(|w| w.meter_ids.as_ref())
.flatten()
.copied()
.collect();
let make_plugin: Box<dyn Fn(Arc<P>) -> AutoPlugin + Send + Sync> =
Box::new(move |_params| AutoPlugin {
layout: layout.clone(),
});
Self {
params,
size,
scale: EditorScale::new(truce_gui::backing_scale()),
font: None,
runtime: None,
make_plugin,
meter_ids,
baseview_window: None,
}
}
}
impl<P: Params + 'static, M: IcedPlugin<P> + 'static> IcedEditor<P, M> {
pub fn new(params: Arc<P>, size: (u32, u32)) -> Self {
Self {
params,
size,
scale: EditorScale::new(truce_gui::backing_scale()),
font: None,
runtime: None,
make_plugin: Box::new(|p| M::new(p)),
meter_ids: Vec::new(),
baseview_window: None,
}
}
#[must_use]
pub fn with_font(mut self, data: &'static [u8]) -> Self {
self.font = Some(data);
self
}
#[must_use]
pub fn with_meter_ids(mut self, ids: Vec<impl Into<u32>>) -> Self {
self.meter_ids = ids.into_iter().map(std::convert::Into::into).collect();
self
}
}
struct IcedRuntime<P: Params, M: IcedPlugin<P>> {
render: Option<RenderState<P, M>>,
cursor_position: Point,
pending_events: Vec<Event>,
program: Option<IcedProgram<P, M>>,
size: (u32, u32),
scale: EditorScale,
last_applied_scale: f64,
font: Option<&'static [u8]>,
}
struct RenderState<P: Params + 'static, M: IcedPlugin<P>> {
device: wgpu::Device,
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
renderer: iced_wgpu::Renderer,
program: IcedProgram<P, M>,
ui_cache: Option<iced_runtime::user_interface::Cache>,
interaction: iced::mouse::Interaction,
viewport: iced_graphics::Viewport,
theme: iced::Theme,
bg_color: Color,
}
impl<P: Params + 'static, M: IcedPlugin<P>> IcedRuntime<P, M> {
#[allow(clippy::needless_pass_by_value)]
fn init_render(&mut self, instance: wgpu::Instance, surface: wgpu::Surface<'static>) -> bool {
let Some(program) = self.program.take() else {
return false;
};
let (lw, lh) = self.size;
let render_scale = self.scale.get();
self.last_applied_scale = render_scale;
let w = truce_gui::to_physical_px(lw, render_scale);
let h = truce_gui::to_physical_px(lh, render_scale);
let adapter =
match pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})) {
Ok(a) => a,
Err(e) => {
log::warn!("no suitable GPU adapter found: {e}");
self.program = Some(program);
return false;
}
};
let (device, queue) =
match pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("truce-iced"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
experimental_features: wgpu::ExperimentalFeatures::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
})) {
Ok(dq) => dq,
Err(e) => {
log::error!("failed to create wgpu device: {e}");
self.program = Some(program);
return false;
}
};
let surface_caps = surface.get_capabilities(&adapter);
if surface_caps.formats.is_empty() {
log::warn!("no surface formats available");
self.program = Some(program);
return false;
}
let surface_format = surface_caps.formats[0];
let alpha_mode = if surface_caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else {
surface_caps.alpha_modes[0]
};
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: w.max(1),
height: h.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: 2,
alpha_mode,
view_formats: vec![],
};
surface.configure(&device, &surface_config);
let surface_device = device.clone();
let engine = iced_wgpu::Engine::new(
&adapter,
device,
queue,
surface_format,
Some(iced_graphics::Antialiasing::MSAAx4),
iced_graphics::Shell::headless(),
);
let default_font = if let Some(data) = self.font {
crate::font::apply_font(data)
} else {
iced::Font::DEFAULT
};
let renderer = iced_wgpu::Renderer::new(engine, default_font, iced::Pixels(14.0));
#[allow(clippy::cast_possible_truncation)]
let viewport =
iced_graphics::Viewport::with_physical_size(Size::new(w, h), render_scale as f32);
let theme = program.plugin.theme();
let bg = crate::theme::truce_dark_theme().palette().background;
self.render = Some(RenderState {
device: surface_device,
surface,
surface_config,
renderer,
program,
ui_cache: Some(iced_runtime::user_interface::Cache::new()),
interaction: iced::mouse::Interaction::default(),
viewport,
theme,
bg_color: bg,
});
log::info!("gpu active (wgpu, {w}x{h})");
true
}
fn tick(&mut self) {
let Some(render) = self.render.as_mut() else {
return;
};
let cur_scale = self.scale.get();
if cur_scale.to_bits() != self.last_applied_scale.to_bits() {
let (lw, lh) = self.size;
let pw = truce_gui::to_physical_px(lw, cur_scale);
let ph = truce_gui::to_physical_px(lh, cur_scale);
render.surface_config.width = pw;
render.surface_config.height = ph;
render
.surface
.configure(&render.device, &render.surface_config);
#[allow(clippy::cast_possible_truncation)] let scale_f32 = cur_scale as f32;
render.viewport =
iced_graphics::Viewport::with_physical_size(Size::new(pw, ph), scale_f32);
self.last_applied_scale = cur_scale;
}
render.program.dispatch(Message::Tick);
let cursor = iced::mouse::Cursor::Available(self.cursor_position);
let logical_size = render.viewport.logical_size();
let style = iced_runtime::core::renderer::Style {
text_color: Color::from_rgb(0.90, 0.90, 0.92),
};
let mut messages: Vec<Message<M::Message>> = Vec::new();
let cache = render
.ui_cache
.take()
.unwrap_or_else(iced_runtime::user_interface::Cache::new);
let view_element = render.program.view();
let mut user_interface = iced_runtime::UserInterface::build(
view_element,
logical_size,
cache,
&mut render.renderer,
);
let pending_events = std::mem::take(&mut self.pending_events);
let (ui_state, _statuses) = user_interface.update(
&pending_events,
cursor,
&mut render.renderer,
&mut iced_runtime::core::clipboard::Null,
&mut messages,
);
if let iced_runtime::user_interface::State::Updated {
mouse_interaction, ..
} = ui_state
{
render.interaction = mouse_interaction;
}
user_interface.draw(&mut render.renderer, &render.theme, &style, cursor);
render.ui_cache = Some(user_interface.into_cache());
for message in messages {
render.program.dispatch(message);
}
let frame = match render.surface.get_current_texture() {
Ok(f) => f,
Err(wgpu::SurfaceError::Timeout | wgpu::SurfaceError::Outdated) => {
render
.surface
.configure(&render.device, &render.surface_config);
return;
}
Err(e) => {
log::warn!("surface error: {e}");
return;
}
};
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let _ = render.renderer.present(
Some(render.bg_color),
render.surface_config.format,
&view,
&render.viewport,
);
frame.present();
}
fn queue_cursor_move(&mut self, x: f32, y: f32) {
self.cursor_position = Point::new(x, y);
self.pending_events
.push(Event::Mouse(iced::mouse::Event::CursorMoved {
position: self.cursor_position,
}));
}
}
struct IcedBaseviewHandler<P: Params + 'static, M: IcedPlugin<P>> {
editor: *mut IcedEditor<P, M>,
last_cursor: Option<baseview::MouseCursor>,
}
unsafe impl<P: Params, M: IcedPlugin<P>> Send for IcedBaseviewHandler<P, M> {}
impl<P: Params + 'static, M: IcedPlugin<P>> Drop for IcedBaseviewHandler<P, M> {
fn drop(&mut self) {
let editor = unsafe { &mut *self.editor };
if let Some(ref mut runtime) = editor.runtime {
runtime.render = None;
}
}
}
#[allow(clippy::match_same_arms)]
fn iced_interaction_to_cursor(interaction: iced::mouse::Interaction) -> baseview::MouseCursor {
use iced::mouse::Interaction;
match interaction {
Interaction::Idle | Interaction::None => baseview::MouseCursor::Default,
Interaction::Pointer | Interaction::Grab => baseview::MouseCursor::Hand,
Interaction::Grabbing => baseview::MouseCursor::HandGrabbing,
Interaction::Text => baseview::MouseCursor::Text,
Interaction::Crosshair => baseview::MouseCursor::Crosshair,
Interaction::NotAllowed => baseview::MouseCursor::NotAllowed,
Interaction::ResizingHorizontally => baseview::MouseCursor::EwResize,
Interaction::ResizingVertically => baseview::MouseCursor::NsResize,
_ => baseview::MouseCursor::Default,
}
}
impl<P: Params + 'static, M: IcedPlugin<P>> baseview::WindowHandler for IcedBaseviewHandler<P, M> {
fn on_frame(&mut self, window: &mut baseview::Window) {
let editor = unsafe { &mut *self.editor };
if let Some(ref mut runtime) = editor.runtime {
runtime.tick();
if let Some(ref render) = runtime.render {
let cursor = iced_interaction_to_cursor(render.interaction);
if self.last_cursor != Some(cursor) {
self.last_cursor = Some(cursor);
window.set_mouse_cursor(cursor);
}
}
}
}
fn on_event(
&mut self,
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
window: &mut baseview::Window,
event: baseview::Event,
) -> baseview::EventStatus {
let editor = unsafe { &mut *self.editor };
let Some(runtime) = editor.runtime.as_mut() else {
return baseview::EventStatus::Ignored;
};
match event {
baseview::Event::Mouse(mouse) => {
match mouse {
baseview::MouseEvent::CursorMoved { position, .. } => {
#[allow(clippy::cast_possible_truncation)]
let pos = (position.x as f32, position.y as f32);
runtime.queue_cursor_move(pos.0, pos.1);
}
baseview::MouseEvent::CursorLeft => {
runtime
.pending_events
.push(Event::Mouse(iced::mouse::Event::CursorLeft));
}
baseview::MouseEvent::ButtonPressed {
button: baseview::MouseButton::Left,
..
} => {
#[cfg(target_os = "windows")]
{
if !window.has_focus() {
window.focus();
}
}
runtime.pending_events.push(Event::Mouse(
iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left),
));
}
baseview::MouseEvent::ButtonReleased {
button: baseview::MouseButton::Left,
..
} => {
runtime.pending_events.push(Event::Mouse(
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left),
));
}
baseview::MouseEvent::WheelScrolled { delta, .. } => {
let dy = match delta {
baseview::ScrollDelta::Lines { y, .. } => y * 30.0,
baseview::ScrollDelta::Pixels { y, .. } => y,
};
runtime.pending_events.push(Event::Mouse(
iced::mouse::Event::WheelScrolled {
delta: iced::mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
},
));
}
_ => return baseview::EventStatus::Ignored,
}
baseview::EventStatus::Captured
}
baseview::Event::Window(baseview::WindowEvent::Resized(info)) => {
crate::platform::note_linux_scale_factor(info.scale());
runtime.scale.set(info.scale());
runtime.last_applied_scale = info.scale();
if let Some(ref mut render) = runtime.render {
let pw = info.physical_size().width;
let ph = info.physical_size().height;
render.surface_config.width = pw.max(1);
render.surface_config.height = ph.max(1);
render
.surface
.configure(&render.device, &render.surface_config);
#[allow(clippy::cast_possible_truncation)] let scale_f32 = info.scale() as f32;
render.viewport =
iced_graphics::Viewport::with_physical_size(Size::new(pw, ph), scale_f32);
}
baseview::EventStatus::Captured
}
_ => baseview::EventStatus::Ignored,
}
}
}
impl<P: Params + 'static, M: IcedPlugin<P>> Editor for IcedEditor<P, M> {
fn size(&self) -> (u32, u32) {
self.size
}
fn open(&mut self, parent: truce_core::editor::RawWindowHandle, context: PluginContext) {
let (w, h) = self.size;
let plugin = (self.make_plugin)(self.params.clone());
let mut param_cache = ParamCache::new(self.params.clone());
if let Some(data) = self.font {
param_cache.set_font(crate::font::apply_font(data));
}
let typed_ctx = context.with_params(self.params.clone());
let program = IcedProgram {
plugin,
param_cache,
context: typed_ctx,
meter_ids: self.meter_ids.clone(),
};
self.runtime = Some(IcedRuntime {
render: None,
cursor_position: Point::ORIGIN,
pending_events: Vec::new(),
program: Some(program),
size: (w, h),
scale: self.scale.clone(),
last_applied_scale: 0.0,
font: self.font,
});
let parent_wrapper = crate::platform::ParentWindow(parent);
let options = baseview::WindowOpenOptions {
title: String::from("truce-iced"),
size: baseview::Size::new(f64::from(w), f64::from(h)),
scale: baseview::WindowScalePolicy::SystemScaleFactor,
};
let editor_addr = std::ptr::from_mut::<IcedEditor<P, M>>(self) as usize;
let window = baseview::Window::open_parented(
&parent_wrapper,
options,
move |window: &mut baseview::Window| {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = unsafe { crate::platform::create_wgpu_surface(&instance, window) };
if let Some(surface) = surface {
let editor = unsafe { &mut *(editor_addr as *mut IcedEditor<P, M>) };
if let Some(ref mut runtime) = editor.runtime {
runtime.init_render(instance, surface);
}
}
IcedBaseviewHandler::<P, M> {
editor: editor_addr as *mut IcedEditor<P, M>,
last_cursor: None,
}
},
);
self.baseview_window = Some(window);
log::info!("editor opened via baseview ({w}x{h})");
}
fn close(&mut self) {
if let Some(mut window) = self.baseview_window.take() {
window.close();
}
self.runtime = None;
log::info!("editor closed");
}
fn idle(&mut self) {
}
fn can_resize(&self) -> bool {
true
}
fn screenshot(
&mut self,
_params: Arc<dyn truce_params::Params>,
) -> Option<(Vec<u8>, u32, u32)> {
let plugin = (self.make_plugin)(Arc::clone(&self.params));
let scale = self.scale.get();
crate::screenshot::render_to_pixels::<P, M>(
Arc::clone(&self.params),
plugin,
self.size,
scale,
self.font,
)
}
fn set_size(&mut self, width: u32, height: u32) -> bool {
self.size = (width, height);
if let Some(ref mut runtime) = self.runtime {
runtime.size = (width, height);
if let Some(ref mut render) = runtime.render {
let scale = self.scale.get();
let pw = truce_gui::to_physical_px(width, scale);
let ph = truce_gui::to_physical_px(height, scale);
#[allow(clippy::cast_possible_truncation)] let scale_f32 = scale as f32;
render.viewport =
iced_graphics::Viewport::with_physical_size(Size::new(pw, ph), scale_f32);
render.surface_config.width = pw;
render.surface_config.height = ph;
render
.surface
.configure(&render.device, &render.surface_config);
}
}
true
}
fn set_scale_factor(&mut self, factor: f64) {
self.scale.set(factor);
}
}