Extract app frame and export dialog helpers

This commit is contained in:
2026-06-16 11:58:22 +02:00
parent d2a841f348
commit 01854f9b10
9 changed files with 786 additions and 584 deletions

View File

@@ -33,37 +33,6 @@
App* App::I = nullptr; // singleton
namespace {
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
{
switch (mode) {
case kCanvasMode::Draw:
return pp::app::CanvasToolMode::draw;
case kCanvasMode::Erase:
return pp::app::CanvasToolMode::erase;
case kCanvasMode::Line:
return pp::app::CanvasToolMode::line;
case kCanvasMode::Camera:
return pp::app::CanvasToolMode::camera;
case kCanvasMode::Grid:
return pp::app::CanvasToolMode::grid;
case kCanvasMode::Copy:
return pp::app::CanvasToolMode::copy;
case kCanvasMode::Cut:
return pp::app::CanvasToolMode::cut;
case kCanvasMode::Fill:
return pp::app::CanvasToolMode::fill;
case kCanvasMode::MaskFree:
return pp::app::CanvasToolMode::mask_free;
case kCanvasMode::MaskLine:
return pp::app::CanvasToolMode::mask_line;
case kCanvasMode::FloodFill:
return pp::app::CanvasToolMode::flood_fill;
default:
return pp::app::CanvasToolMode::draw;
}
}
void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
const auto status = pp::renderer::gl::apply_opengl_viewport(
@@ -215,6 +184,9 @@ namespace pp::panopainter
{
bool process_legacy_recording_worker_iteration(App& app);
void update_legacy_recording_frame_label(App& app);
bool update_legacy_app_ui_observer(App& app, Node* n);
void watch_legacy_app_ui_children(App& app, const std::function<bool(Node*)>& observer, bool skip_first_main_child);
void update_legacy_canvas_toolbar(App& app);
}
bool App::check_license()
@@ -359,67 +331,7 @@ void App::async_swap()
bool App::update_ui_observer(Node *n)
{
std::vector<pp::app::AppUiObserverParentClip> parent_clips;
if (n) {
for (Node* p = n->m_parent; p; p = p->m_parent) {
parent_clips.push_back(pp::app::AppUiObserverParentClip {
.clip = pp::app::AppUiObserverRect {
.x = p->m_clip_uncut.x,
.y = p->m_clip_uncut.y,
.width = p->m_clip_uncut.z,
.height = p->m_clip_uncut.w,
},
.padding_top = YGNodeLayoutGetPadding(p->y_node, YGEdgeTop),
.padding_right = YGNodeLayoutGetPadding(p->y_node, YGEdgeRight),
.padding_bottom = YGNodeLayoutGetPadding(p->y_node, YGEdgeBottom),
.padding_left = YGNodeLayoutGetPadding(p->y_node, YGEdgeLeft),
});
}
}
const auto plan = pp::app::plan_app_ui_observer(
n != nullptr,
n && n->m_display,
n && n->m_on_screen,
n
? pp::app::AppUiObserverRect {
.x = n->m_clip_uncut.x,
.y = n->m_clip_uncut.y,
.width = n->m_clip_uncut.z,
.height = n->m_clip_uncut.w,
}
: pp::app::AppUiObserverRect {},
parent_clips,
height,
zoom,
off_x,
off_y);
if (!plan) {
LOG("UI observer plan failed: %s", plan.status().message);
return false;
}
if (!n)
return false;
if (plan.value().notify_leave_screen)
n->handle_on_screen(true, false);
if (plan.value().notify_enter_screen)
n->handle_on_screen(false, true);
n->m_on_screen = plan.value().next_on_screen;
if (!plan.value().draw_node)
return false;
apply_app_scissor(pp::renderer::gl::OpenGlScissorRect {
.enabled = 1U,
.x = plan.value().scissor_x,
.y = plan.value().scissor_y,
.width = plan.value().scissor_width,
.height = plan.value().scissor_height,
});
n->draw();
return true;
return pp::panopainter::update_legacy_app_ui_observer(*this, n);
}
void App::draw(float dt)
@@ -446,11 +358,7 @@ void App::draw(float dt)
.height = static_cast<std::int32_t>(uirtt.getHeight()),
});
apply_app_scissor_test(true);
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
layout_designer[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
pp::panopainter::watch_legacy_app_ui_children(*this, observer, true);
apply_app_scissor_test(false);
uirtt.unbindFramebuffer();
}
@@ -465,11 +373,7 @@ void App::draw(float dt)
.height = static_cast<std::int32_t>(height),
});
apply_app_scissor_test(true);
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
layout_designer[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
pp::panopainter::watch_legacy_app_ui_children(*this, observer, false);
apply_app_scissor_test(false);
}
@@ -492,28 +396,7 @@ void App::update(float dt)
if (!update_plan.refresh_canvas_toolbar)
return;
{
auto mode = Canvas::I->m_current_mode;
CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
const auto toolbar = pp::app::plan_canvas_tool_button_state(
canvas_tool_mode_from_canvas_mode(mode),
pm && pm->m_picking,
canvas->m_canvas->m_touch_lock);
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
}
pp::panopainter::update_legacy_canvas_toolbar(*this);
}
void App::terminate()

View File

@@ -16,13 +16,6 @@
#include "node_dialog_browse.h"
#include "node_dialog_resize.h"
#include "node_dialog_cloud.h"
#include "node_dialog_export_ppbr.h"
#include <codec_api.h>
#define MP4V2_NO_STDINT_DEFS
#include <mp4v2/mp4v2.h>
#include <utility>
#ifdef __QUEST__
#include "oculus_vr.h"
@@ -34,95 +27,18 @@ void open_changelog_dialog(App& app);
void open_about_dialog(App& app);
void open_whatsnew_dialog(App& app, bool force_show);
void open_shortcuts_dialog(App& app);
void open_document_export_dialog(App& app, std::string ext);
void open_document_export_layers_dialog(App& app);
void open_document_export_anim_frames_dialog(App& app);
void open_document_export_depth_dialog(App& app);
void open_document_export_cube_faces_dialog(App& app);
void open_document_timelapse_export_dialog(App& app);
void open_document_export_mp4_dialog(App& app);
void open_ppbr_export_dialog(App& app);
}
namespace {
[[nodiscard]] bool can_start_document_export(App& app, bool requires_license)
{
const auto decision = pp::app::plan_document_export_start(
requires_license,
!requires_license || app.check_license(),
app.canvas != nullptr);
switch (decision) {
case pp::app::DocumentExportStartDecision::start_now:
return true;
case pp::app::DocumentExportStartDecision::show_license_disabled:
{
const auto plan = pp::app::plan_document_export_license_disabled_dialog();
app.message_box(plan.title, plan.message, plan.show_cancel);
return false;
}
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
return false;
}
return false;
}
void start_document_export_collection(
App& app,
pp::app::DocumentExportCollectionKind kind)
{
const auto plan = pp::app::plan_document_export_collection_target(
kind,
app.uses_work_directory_document_export_collections());
const auto success_kind = pp::app::document_export_collection_success_kind(kind);
if (plan.destination == pp::app::DocumentExportCollectionDestination::work_directory_collection) {
const auto target = pp::app::make_document_export_collection_target(
app.work_path,
app.doc_name,
plan.suffix);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(success_kind, target.status().message);
app.message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
const auto status = pp::panopainter::execute_legacy_document_export_collection(
app,
plan.kind,
target.value());
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
kind == pp::app::DocumentExportCollectionKind::layers
? pp::app::DocumentExportExecutionKind::layers_collection
: pp::app::DocumentExportExecutionKind::animation_frames_collection),
status.message);
return;
}
app.pick_dir([
&app,
kind = plan.kind,
success_kind
](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, app.doc_name);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(success_kind, target.status().message);
app.message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
const auto status = pp::panopainter::execute_legacy_document_export_stem(
app,
kind,
target.value());
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
kind == pp::app::DocumentExportCollectionKind::layers
? pp::app::DocumentExportExecutionKind::layers_stem
: pp::app::DocumentExportExecutionKind::animation_frames_stem),
status.message);
});
}
void wire_document_browse_dialog_actions(
App& app,
const std::shared_ptr<NodeDialogBrowse>& dialog,
@@ -453,59 +369,22 @@ void App::dialog_save()
void App::dialog_export(std::string ext)
{
if (!can_start_document_export(*this, true))
return;
// TODO: use picker
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(
pp::app::DocumentExportSuccessKind::equirectangular,
target.status().message);
message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
const auto status = pp::panopainter::execute_legacy_document_export_file(*this, target.value());
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
pp::app::DocumentExportExecutionKind::equirectangular_file),
status.message);
pp::panopainter::open_document_export_dialog(*this, ext);
}
void App::dialog_export_layers()
{
if (!can_start_document_export(*this, true))
return;
start_document_export_collection(
*this,
pp::app::DocumentExportCollectionKind::layers);
pp::panopainter::open_document_export_layers_dialog(*this);
}
void App::dialog_export_anim_frames()
{
if (!can_start_document_export(*this, true))
return;
start_document_export_collection(
*this,
pp::app::DocumentExportCollectionKind::animation_frames);
pp::panopainter::open_document_export_anim_frames_dialog(*this);
}
void App::dialog_export_depth()
{
if (!can_start_document_export(*this, true))
return;
const auto status = pp::panopainter::execute_legacy_document_export_depth(*this, doc_name);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(pp::app::DocumentExportExecutionKind::depth),
status.message);
pp::panopainter::open_document_export_depth_dialog(*this);
}
void App::dialog_resize()
@@ -551,15 +430,7 @@ void App::dialog_resize()
void App::dialog_export_cube_faces()
{
if (!can_start_document_export(*this, false))
return;
const auto status = pp::panopainter::execute_legacy_document_export_cube_faces(*this, doc_name);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(pp::app::DocumentExportExecutionKind::cube_faces),
status.message);
pp::panopainter::open_document_export_cube_faces_dialog(*this);
}
void App::dialog_layer_rename()
@@ -613,169 +484,17 @@ void App::dialog_preset_download()
void App::dialog_ppbr_export()
{
auto* overlay_anchor = layout[main_id];
if (!overlay_anchor) {
LOG("PPBR export dialog open failed: main layout anchor is missing");
return;
}
auto dialog = pp::panopainter::make_legacy_overlay_node<NodeDialogExportPPBR>(*this);
const auto overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*overlay_anchor, dialog);
if (!overlay) {
LOG("PPBR export dialog open failed: %s", overlay.status().message);
return;
}
const auto overlay_handle = overlay.value();
const auto close_dialog = [overlay_anchor, overlay_handle]() {
const auto close_status = pp::panopainter::close_legacy_overlay_node(*overlay_anchor, overlay_handle);
(void)close_status;
};
dialog->btn_ok->on_click = [this, dialog] (Node*) {
const auto request = pp::panopainter::make_legacy_brush_package_export_request(*dialog);
if (uses_prepared_file_writes())
{
pick_file_save("ppbr", "exported-brushes",
[this, dialog, request] (std::string path) {
const auto status = pp::panopainter::execute_legacy_brush_package_export(
*this,
*dialog,
request,
path,
pp::panopainter::LegacyBrushPackageExportMode::inline_export_only);
if (!status.ok())
LOG("PPBR export failed: %s", status.message);
},
[dialog] (const std::string& path, bool saved) {
(void)path;
pp::panopainter::complete_legacy_brush_package_export(*dialog, saved);
}
);
return;
}
pick_file_save({ "ppbr" }, [this, dialog, request] (std::string path) {
const auto status = pp::panopainter::execute_legacy_brush_package_export(
*this,
*dialog,
request,
path,
pp::panopainter::LegacyBrushPackageExportMode::desktop_async_close_and_message);
if (!status.ok())
LOG("PPBR export failed: %s", status.message);
});
};
dialog->btn_cancel->on_click = [close_dialog](Node*)
{
close_dialog();
};
pp::panopainter::open_ppbr_export_dialog(*this);
}
void App::dialog_timelapse_export()
{
if (!can_start_document_export(*this, false))
return;
if (uses_prepared_file_writes())
{
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-timelapse");
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(
pp::app::DocumentExportSuccessKind::timelapse,
target.status().message);
message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) {
const auto status = pp::panopainter::execute_legacy_document_video_export(
*this,
pp::app::DocumentVideoExportKind::timelapse,
path,
false);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
pp::app::DocumentExportExecutionKind::timelapse),
status.message);
},
[](const std::string& path, bool saved) {
(void)path;
(void)saved;
}
);
return;
}
pick_file_save({ "mp4" }, [this](std::string path) {
const auto status = pp::panopainter::execute_legacy_document_video_export(
*this,
pp::app::DocumentVideoExportKind::timelapse,
path,
true);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(pp::app::DocumentExportExecutionKind::timelapse),
status.message);
});
pp::panopainter::open_document_timelapse_export_dialog(*this);
}
void App::dialog_export_mp4()
{
if (!can_start_document_export(*this, false))
return;
if (uses_prepared_file_writes())
{
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-animation");
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(
pp::app::DocumentExportSuccessKind::animation_mp4,
target.status().message);
message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) {
const auto status = pp::panopainter::execute_legacy_document_video_export(
*this,
pp::app::DocumentVideoExportKind::animation_mp4,
path,
false);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
pp::app::DocumentExportExecutionKind::animation_mp4),
status.message);
},
[](const std::string& path, bool saved) {
(void)path;
(void)saved;
}
);
return;
}
pick_file_save({ "mp4" }, [this](std::string path) {
const auto status = pp::panopainter::execute_legacy_document_video_export(
*this,
pp::app::DocumentVideoExportKind::animation_mp4,
path,
true);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(pp::app::DocumentExportExecutionKind::animation_mp4),
status.message);
});
pp::panopainter::open_document_export_mp4_dialog(*this);
}
void App::dialog_whatsnew(bool force_show)

316
src/app_dialogs_export.cpp Normal file
View File

@@ -0,0 +1,316 @@
#include "pch.h"
#include "app.h"
#include "app_core/app_dialog.h"
#include "app_core/document_export.h"
#include "legacy_brush_package_export_services.h"
#include "legacy_document_export_services.h"
#include "legacy_ui_overlay_services.h"
#include "node_dialog_export_ppbr.h"
#include <codec_api.h>
#define MP4V2_NO_STDINT_DEFS
#include <mp4v2/mp4v2.h>
#include <utility>
namespace pp::panopainter {
namespace {
[[nodiscard]] bool can_start_document_export(App& app, bool requires_license)
{
const auto decision = pp::app::plan_document_export_start(
requires_license,
!requires_license || app.check_license(),
app.canvas != nullptr);
switch (decision) {
case pp::app::DocumentExportStartDecision::start_now:
return true;
case pp::app::DocumentExportStartDecision::show_license_disabled:
{
const auto plan = pp::app::plan_document_export_license_disabled_dialog();
app.message_box(plan.title, plan.message, plan.show_cancel);
return false;
}
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
return false;
}
return false;
}
void start_document_export_collection(
App& app,
pp::app::DocumentExportCollectionKind kind)
{
const auto plan = pp::app::plan_document_export_collection_target(
kind,
app.uses_work_directory_document_export_collections());
const auto success_kind = pp::app::document_export_collection_success_kind(kind);
if (plan.destination == pp::app::DocumentExportCollectionDestination::work_directory_collection) {
const auto target = pp::app::make_document_export_collection_target(
app.work_path,
app.doc_name,
plan.suffix);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(success_kind, target.status().message);
app.message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
const auto status = pp::panopainter::execute_legacy_document_export_collection(
app,
plan.kind,
target.value());
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
kind == pp::app::DocumentExportCollectionKind::layers
? pp::app::DocumentExportExecutionKind::layers_collection
: pp::app::DocumentExportExecutionKind::animation_frames_collection),
status.message);
return;
}
app.pick_dir([
&app,
kind = plan.kind,
success_kind
](std::string path) {
const auto target = pp::app::make_document_export_stem_target(path, app.doc_name);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(success_kind, target.status().message);
app.message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
const auto status = pp::panopainter::execute_legacy_document_export_stem(
app,
kind,
target.value());
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
kind == pp::app::DocumentExportCollectionKind::layers
? pp::app::DocumentExportExecutionKind::layers_stem
: pp::app::DocumentExportExecutionKind::animation_frames_stem),
status.message);
});
}
void start_document_video_export(
App& app,
pp::app::DocumentVideoExportKind kind,
pp::app::DocumentExportSuccessKind success_kind,
const char* suffix,
pp::app::DocumentExportExecutionKind execution_kind)
{
if (!can_start_document_export(app, false))
return;
if (app.uses_prepared_file_writes())
{
const auto target = pp::app::make_document_export_suggested_name(app.doc_name, suffix);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(
success_kind,
target.status().message);
app.message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
app.pick_file_save("mp4", target.value().name,
[&app, kind, execution_kind](std::string path) {
const auto status = pp::panopainter::execute_legacy_document_video_export(
app,
kind,
path,
false);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(execution_kind),
status.message);
},
[](const std::string& path, bool saved) {
(void)path;
(void)saved;
}
);
return;
}
app.pick_file_save({ "mp4" }, [&app, kind, execution_kind](std::string path) {
const auto status = pp::panopainter::execute_legacy_document_video_export(
app,
kind,
path,
true);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(execution_kind),
status.message);
});
}
} // namespace
void open_document_export_dialog(App& app, std::string ext)
{
if (!can_start_document_export(app, true))
return;
// TODO: use picker
const auto target = pp::app::make_document_export_file_target(app.work_path, app.doc_name, ext);
if (!target) {
const auto dialog = pp::app::plan_document_export_failure_dialog(
pp::app::DocumentExportSuccessKind::equirectangular,
target.status().message);
app.message_box(dialog.title, dialog.message, dialog.show_cancel);
return;
}
const auto status = pp::panopainter::execute_legacy_document_export_file(app, target.value());
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(
pp::app::DocumentExportExecutionKind::equirectangular_file),
status.message);
}
void open_document_export_layers_dialog(App& app)
{
if (!can_start_document_export(app, true))
return;
start_document_export_collection(
app,
pp::app::DocumentExportCollectionKind::layers);
}
void open_document_export_anim_frames_dialog(App& app)
{
if (!can_start_document_export(app, true))
return;
start_document_export_collection(
app,
pp::app::DocumentExportCollectionKind::animation_frames);
}
void open_document_export_depth_dialog(App& app)
{
if (!can_start_document_export(app, true))
return;
const auto status = pp::panopainter::execute_legacy_document_export_depth(app, app.doc_name);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(pp::app::DocumentExportExecutionKind::depth),
status.message);
}
void open_document_export_cube_faces_dialog(App& app)
{
if (!can_start_document_export(app, false))
return;
const auto status = pp::panopainter::execute_legacy_document_export_cube_faces(app, app.doc_name);
if (!status.ok())
LOG(
"%s: %s",
pp::app::document_export_execution_log_message(pp::app::DocumentExportExecutionKind::cube_faces),
status.message);
}
void open_document_timelapse_export_dialog(App& app)
{
start_document_video_export(
app,
pp::app::DocumentVideoExportKind::timelapse,
pp::app::DocumentExportSuccessKind::timelapse,
"-timelapse",
pp::app::DocumentExportExecutionKind::timelapse);
}
void open_document_export_mp4_dialog(App& app)
{
start_document_video_export(
app,
pp::app::DocumentVideoExportKind::animation_mp4,
pp::app::DocumentExportSuccessKind::animation_mp4,
"-animation",
pp::app::DocumentExportExecutionKind::animation_mp4);
}
void open_ppbr_export_dialog(App& app)
{
auto* overlay_anchor = app.layout[app.main_id];
if (!overlay_anchor) {
LOG("PPBR export dialog open failed: main layout anchor is missing");
return;
}
auto dialog = pp::panopainter::make_legacy_overlay_node<NodeDialogExportPPBR>(app);
const auto overlay = pp::panopainter::open_legacy_overlay_node_with_handle(*overlay_anchor, dialog);
if (!overlay) {
LOG("PPBR export dialog open failed: %s", overlay.status().message);
return;
}
const auto overlay_handle = overlay.value();
const auto close_dialog = [overlay_anchor, overlay_handle]() {
const auto close_status = pp::panopainter::close_legacy_overlay_node(*overlay_anchor, overlay_handle);
(void)close_status;
};
dialog->btn_ok->on_click = [&app, dialog] (Node*) {
const auto request = pp::panopainter::make_legacy_brush_package_export_request(*dialog);
if (app.uses_prepared_file_writes())
{
app.pick_file_save("ppbr", "exported-brushes",
[&app, dialog, request] (std::string path) {
const auto status = pp::panopainter::execute_legacy_brush_package_export(
app,
*dialog,
request,
path,
pp::panopainter::LegacyBrushPackageExportMode::inline_export_only);
if (!status.ok())
LOG("PPBR export failed: %s", status.message);
},
[dialog] (const std::string& path, bool saved) {
(void)path;
pp::panopainter::complete_legacy_brush_package_export(*dialog, saved);
}
);
return;
}
app.pick_file_save({ "ppbr" }, [&app, dialog, request] (std::string path) {
const auto status = pp::panopainter::execute_legacy_brush_package_export(
app,
*dialog,
request,
path,
pp::panopainter::LegacyBrushPackageExportMode::desktop_async_close_and_message);
if (!status.ok())
LOG("PPBR export failed: %s", status.message);
});
};
dialog->btn_cancel->on_click = [close_dialog](Node*)
{
close_dialog();
};
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,164 @@
#include "pch.h"
#include "app.h"
#include "app_core/app_frame.h"
#include "app_core/canvas_tool_ui.h"
#include "legacy_ui_gl_dispatch.h"
#include "renderer_gl/opengl_capabilities.h"
namespace {
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
{
switch (mode) {
case kCanvasMode::Draw:
return pp::app::CanvasToolMode::draw;
case kCanvasMode::Erase:
return pp::app::CanvasToolMode::erase;
case kCanvasMode::Line:
return pp::app::CanvasToolMode::line;
case kCanvasMode::Camera:
return pp::app::CanvasToolMode::camera;
case kCanvasMode::Grid:
return pp::app::CanvasToolMode::grid;
case kCanvasMode::Copy:
return pp::app::CanvasToolMode::copy;
case kCanvasMode::Cut:
return pp::app::CanvasToolMode::cut;
case kCanvasMode::Fill:
return pp::app::CanvasToolMode::fill;
case kCanvasMode::MaskFree:
return pp::app::CanvasToolMode::mask_free;
case kCanvasMode::MaskLine:
return pp::app::CanvasToolMode::mask_line;
case kCanvasMode::FloodFill:
return pp::app::CanvasToolMode::flood_fill;
default:
return pp::app::CanvasToolMode::draw;
}
}
void apply_legacy_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_rect(
scissor,
pp::renderer::gl::OpenGlScissorDispatch {
.enable = pp::legacy::ui_gl::enable_opengl_state,
.disable = pp::legacy::ui_gl::disable_opengl_state,
.scissor = pp::legacy::ui_gl::set_opengl_scissor,
});
if (!status.ok())
LOG("OpenGL scissor failed: %s", status.message);
}
void watch_layout_children(LayoutManager* layout, uint16_t main_id, int start_index, const std::function<bool(Node*)>& observer)
{
if (!layout)
return;
if (auto* root = layout->get(main_id)) {
for (int i = start_index; i < static_cast<int>(root->m_children.size()); ++i)
root->m_children[i]->watch(observer);
}
}
}
namespace pp::panopainter
{
bool update_legacy_app_ui_observer(App& app, Node* n)
{
std::vector<pp::app::AppUiObserverParentClip> parent_clips;
if (n) {
for (Node* p = n->m_parent; p; p = p->m_parent) {
parent_clips.push_back(pp::app::AppUiObserverParentClip {
.clip = pp::app::AppUiObserverRect {
.x = p->m_clip_uncut.x,
.y = p->m_clip_uncut.y,
.width = p->m_clip_uncut.z,
.height = p->m_clip_uncut.w,
},
.padding_top = YGNodeLayoutGetPadding(p->y_node, YGEdgeTop),
.padding_right = YGNodeLayoutGetPadding(p->y_node, YGEdgeRight),
.padding_bottom = YGNodeLayoutGetPadding(p->y_node, YGEdgeBottom),
.padding_left = YGNodeLayoutGetPadding(p->y_node, YGEdgeLeft),
});
}
}
const auto plan = pp::app::plan_app_ui_observer(
n != nullptr,
n && n->m_display,
n && n->m_on_screen,
n
? pp::app::AppUiObserverRect {
.x = n->m_clip_uncut.x,
.y = n->m_clip_uncut.y,
.width = n->m_clip_uncut.z,
.height = n->m_clip_uncut.w,
}
: pp::app::AppUiObserverRect {},
parent_clips,
app.height,
app.zoom,
app.off_x,
app.off_y);
if (!plan) {
LOG("UI observer plan failed: %s", plan.status().message);
return false;
}
if (!n)
return false;
if (plan.value().notify_leave_screen)
n->handle_on_screen(true, false);
if (plan.value().notify_enter_screen)
n->handle_on_screen(false, true);
n->m_on_screen = plan.value().next_on_screen;
if (!plan.value().draw_node)
return false;
apply_legacy_app_scissor(pp::renderer::gl::OpenGlScissorRect {
.enabled = 1U,
.x = plan.value().scissor_x,
.y = plan.value().scissor_y,
.width = plan.value().scissor_width,
.height = plan.value().scissor_height,
});
n->draw();
return true;
}
void watch_legacy_app_ui_children(App& app, const std::function<bool(Node*)>& observer, bool skip_first_main_child)
{
watch_layout_children(&app.layout, app.main_id, skip_first_main_child ? 1 : 0, observer);
watch_layout_children(&app.layout_designer, app.main_id, 0, observer);
}
void update_legacy_canvas_toolbar(App& app)
{
const auto mode = Canvas::I->m_current_mode;
auto* pm = app.canvas->m_canvas->get_mode<CanvasModePen>();
const auto toolbar = pp::app::plan_canvas_tool_button_state(
canvas_tool_mode_from_canvas_mode(mode),
pm && pm->m_picking,
app.canvas->m_canvas->m_touch_lock);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
app.layout[app.main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
app.layout[app.main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
app.layout[app.main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
app.layout[app.main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
app.layout[app.main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
}
}

View File

@@ -811,6 +811,82 @@ inline void execute_legacy_canvas_draw_merge_layer_path(
};
}
template <
typename BindBlenderFramebuffer,
typename ClearBlenderFramebuffer,
typename UnbindBlenderFramebuffer,
typename BindSampler,
typename BindNearestSampler,
typename BindStencilSampler,
typename SetActiveTextureUnit,
typename BindTemporaryTexture,
typename UnbindTemporaryTexture,
typename BindSmaskTexture,
typename UnbindSmaskTexture,
typename BindTemporaryDualTexture,
typename UnbindTemporaryDualTexture,
typename BindPatternTexture,
typename DrawFace,
typename BindBlenderTexture,
typename UnbindBlenderTexture,
typename BindDestinationTexture,
typename UnbindDestinationTexture,
typename CopyDestinationFramebuffer,
typename DrawDebugOutline,
typename DrawFrame>
[[nodiscard]] inline LegacyCanvasDrawMergeLayerPathExecution make_legacy_canvas_draw_merge_layer_path_gl_execution(
const LegacyCanvasDrawMergeLayerPathGlUniforms& uniforms,
BindBlenderFramebuffer&& bind_blender_framebuffer,
ClearBlenderFramebuffer&& clear_blender_framebuffer,
UnbindBlenderFramebuffer&& unbind_blender_framebuffer,
BindSampler&& bind_sampler,
BindNearestSampler&& bind_nearest_sampler,
BindStencilSampler&& bind_stencil_sampler,
SetActiveTextureUnit&& set_active_texture_unit,
BindTemporaryTexture&& bind_temporary_texture,
UnbindTemporaryTexture&& unbind_temporary_texture,
BindSmaskTexture&& bind_smask_texture,
UnbindSmaskTexture&& unbind_smask_texture,
BindTemporaryDualTexture&& bind_temporary_dual_texture,
UnbindTemporaryDualTexture&& unbind_temporary_dual_texture,
BindPatternTexture&& bind_pattern_texture,
DrawFace&& draw_face,
BindBlenderTexture&& bind_blender_texture,
UnbindBlenderTexture&& unbind_blender_texture,
BindDestinationTexture&& bind_destination_texture,
UnbindDestinationTexture&& unbind_destination_texture,
CopyDestinationFramebuffer&& copy_destination_framebuffer,
DrawDebugOutline&& draw_debug_outline,
DrawFrame&& draw_frame)
{
return make_legacy_canvas_draw_merge_layer_path_gl_execution(
uniforms,
LegacyCanvasDrawMergeLayerPathGlExecution {
.bind_blender_framebuffer = std::forward<BindBlenderFramebuffer>(bind_blender_framebuffer),
.clear_blender_framebuffer = std::forward<ClearBlenderFramebuffer>(clear_blender_framebuffer),
.unbind_blender_framebuffer = std::forward<UnbindBlenderFramebuffer>(unbind_blender_framebuffer),
.bind_sampler = std::forward<BindSampler>(bind_sampler),
.bind_nearest_sampler = std::forward<BindNearestSampler>(bind_nearest_sampler),
.bind_stencil_sampler = std::forward<BindStencilSampler>(bind_stencil_sampler),
.set_active_texture_unit = std::forward<SetActiveTextureUnit>(set_active_texture_unit),
.bind_temporary_texture = std::forward<BindTemporaryTexture>(bind_temporary_texture),
.unbind_temporary_texture = std::forward<UnbindTemporaryTexture>(unbind_temporary_texture),
.bind_smask_texture = std::forward<BindSmaskTexture>(bind_smask_texture),
.unbind_smask_texture = std::forward<UnbindSmaskTexture>(unbind_smask_texture),
.bind_temporary_dual_texture = std::forward<BindTemporaryDualTexture>(bind_temporary_dual_texture),
.unbind_temporary_dual_texture = std::forward<UnbindTemporaryDualTexture>(unbind_temporary_dual_texture),
.bind_pattern_texture = std::forward<BindPatternTexture>(bind_pattern_texture),
.draw_face = std::forward<DrawFace>(draw_face),
.bind_blender_texture = std::forward<BindBlenderTexture>(bind_blender_texture),
.unbind_blender_texture = std::forward<UnbindBlenderTexture>(unbind_blender_texture),
.bind_destination_texture = std::forward<BindDestinationTexture>(bind_destination_texture),
.unbind_destination_texture = std::forward<UnbindDestinationTexture>(unbind_destination_texture),
.copy_destination_framebuffer = std::forward<CopyDestinationFramebuffer>(copy_destination_framebuffer),
.draw_debug_outline = std::forward<DrawDebugOutline>(draw_debug_outline),
.draw_frame = std::forward<DrawFrame>(draw_frame),
});
}
template <typename PlanOnionRange, typename ShouldDrawPlane, typename VisitLayerPlane, typename LogOnionRangeFailure>
inline void execute_legacy_canvas_draw_layer_traversal(
size_t layer_count,

View File

@@ -231,6 +231,170 @@ void run_canvas_tool_mode(pp::app::CanvasToolMode mode)
LOG("Canvas input tool action failed: %s", status.message);
}
pp::panopainter::LegacyCanvasDrawMergeLayerPathExecution make_node_canvas_layer_path_execution(
NodeCanvas& node_canvas,
size_t layer_index,
int plane_index,
const glm::mat4& plane_mvp_z,
const Brush* brush,
bool copy_blend_destination,
bool use_nearest_sampler)
{
auto* layer = node_canvas.m_canvas->m_layers[layer_index].get();
const auto plane_mvp = plane_mvp_z;
const auto brush_ptr = brush;
const auto smask_active = node_canvas.m_canvas->m_smask_active;
auto draw_layer_frame = pp::panopainter::make_legacy_canvas_draw_merge_layer_frame_draw(
layer,
&node_canvas.m_face_plane,
set_active_texture_unit,
plane_index,
layer->m_opacity);
glm::vec2 patt_scale = glm::vec2(brush_ptr->m_pattern_scale);
if (brush_ptr->m_pattern_flipx)
patt_scale.x *= -1.f;
if (brush_ptr->m_pattern_flipy)
patt_scale.y *= -1.f;
return pp::panopainter::make_legacy_canvas_draw_merge_layer_path_gl_execution(
{
.temporary_erase = {
.mvp = plane_mvp,
.texture_slot = 0,
.stroke_texture_slot = 1,
.mask_texture_slot = 2,
.mask_enabled = smask_active,
},
.temporary_paint = {
.resolution = Canvas::I->m_size,
.pattern = {
.scale = patt_scale,
.invert = static_cast<float>(brush_ptr->m_pattern_invert),
.brightness = brush_ptr->m_pattern_brightness,
.contrast = brush_ptr->m_pattern_contrast,
.depth = brush_ptr->m_pattern_depth,
.blend_mode = brush_ptr->m_pattern_blend_mode,
.offset = Canvas::I->m_pattern_offset,
},
.mvp = plane_mvp,
.layer_alpha = 1.0f,
.alpha_lock = layer->m_alpha_locked,
.mask_enabled = smask_active,
.use_fragcoord = false,
.blend_mode = brush_ptr->m_blend_mode,
.use_dual = brush_ptr->m_dual_enabled,
.dual_blend_mode = brush_ptr->m_dual_blend_mode,
.dual_alpha = brush_ptr->m_dual_opacity,
.use_pattern = brush_ptr->m_pattern_enabled && !brush_ptr->m_pattern_eachsample,
},
.layer_texture = {
.mvp = plane_mvp,
.texture_slot = 0,
.alpha = 1.f,
.highlight = layer->m_hightlight,
},
.blend = {
.shader = {
.mvp = glm::ortho(-1, 1, -1, 1),
.texture_slot = 0,
.destination_texture_slot = 2,
.use_destination_texture = copy_blend_destination,
.blend_mode = layer->m_blend_mode,
.alpha = 1.f,
},
.copy_destination = copy_blend_destination,
},
.use_nearest_sampler = use_nearest_sampler,
.use_dual_texture = brush_ptr->m_dual_enabled,
},
[&node_canvas] {
node_canvas.m_blender_rtt.bindFramebuffer();
},
[&node_canvas] {
node_canvas.m_blender_rtt.clear();
},
[&node_canvas] {
node_canvas.m_blender_rtt.unbindFramebuffer();
},
[&node_canvas](int unit) {
node_canvas.m_sampler.bind(unit);
},
[&node_canvas](int unit) {
node_canvas.m_sampler_nearest.bind(unit);
},
[&node_canvas](int unit) {
node_canvas.m_sampler_stencil.bind(unit);
},
[](int unit) {
set_active_texture_unit(unit);
},
[&node_canvas, plane_index] {
node_canvas.m_canvas->m_tmp[plane_index].bindTexture();
},
[&node_canvas, plane_index] {
node_canvas.m_canvas->m_tmp[plane_index].unbindTexture();
},
[&node_canvas, plane_index] {
node_canvas.m_canvas->m_smask.rtt(plane_index).bindTexture();
},
[&node_canvas, plane_index] {
node_canvas.m_canvas->m_smask.rtt(plane_index).unbindTexture();
},
[&node_canvas, plane_index] {
node_canvas.m_canvas->m_tmp_dual[plane_index].bindTexture();
},
[&node_canvas, plane_index] {
node_canvas.m_canvas->m_tmp_dual[plane_index].unbindTexture();
},
[brush_ptr] {
brush_ptr->m_pattern_texture ? brush_ptr->m_pattern_texture->bind() : unbind_texture_2d();
},
[&node_canvas] {
node_canvas.m_face_plane.draw_fill();
},
[&node_canvas] {
node_canvas.m_blender_rtt.bindTexture();
},
[&node_canvas] {
node_canvas.m_blender_rtt.unbindTexture();
},
[&node_canvas] {
node_canvas.m_blender_bg.bind();
},
[&node_canvas] {
node_canvas.m_blender_bg.unbind();
},
[&node_canvas] {
copy_framebuffer_to_texture_2d(
0,
0,
0,
0,
node_canvas.m_blender_bg.size().x,
node_canvas.m_blender_bg.size().y);
},
#ifdef _DEBUG
[&node_canvas, layer_index, plane_index, plane_mvp] {
auto bb = node_canvas.m_canvas->m_layers[layer_index]->box(plane_index) / (float)node_canvas.m_canvas->m_layers[layer_index]->w;
glm::vec2 bbmin = xy(bb);
glm::vec2 bbsz = zw(bb) - xy(bb);
pp::panopainter::configure_legacy_ui_color_shader(
plane_mvp
* glm::translate(glm::vec3(bbmin * 2.f, 0))
* glm::translate(glm::vec3(-1, -1, 0))
* glm::scale(glm::vec3(bbsz, 1))
* glm::translate(glm::vec3(1, 1, 0)),
{ 1, 0, 0, 1 });
node_canvas.m_face_plane.draw_stroke();
},
#else
[] {
},
#endif
draw_layer_frame);
}
}
Node* NodeCanvas::clone_instantiate() const
@@ -467,161 +631,14 @@ void NodeCanvas::draw()
glm::eulerAngleYXZ(yaw, pitch, roll) *
m_canvas->m_plane_transform[plane_index] *
glm::translate(glm::vec3(0, 0, -1));
const auto draw_layer_frame = pp::panopainter::make_legacy_canvas_draw_merge_layer_frame_draw(
m_canvas->m_layers[layer_index].get(),
&m_face_plane,
set_active_texture_unit,
const auto layer_path_execution = make_node_canvas_layer_path_execution(
*this,
layer_index,
plane_index,
m_canvas->m_layers[layer_index]->m_opacity);
glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
if (b->m_pattern_flipx)
patt_scale.x *= -1.f;
if (b->m_pattern_flipy)
patt_scale.y *= -1.f;
const auto layer_path_execution = pp::panopainter::make_legacy_canvas_draw_merge_layer_path_gl_execution(
{
.temporary_erase = {
.mvp = plane_mvp_z,
.texture_slot = 0,
.stroke_texture_slot = 1,
.mask_texture_slot = 2,
.mask_enabled = m_canvas->m_smask_active,
},
.temporary_paint = {
.resolution = Canvas::I->m_size,
.pattern = {
.scale = patt_scale,
.invert = static_cast<float>(b->m_pattern_invert),
.brightness = b->m_pattern_brightness,
.contrast = b->m_pattern_contrast,
.depth = b->m_pattern_depth,
.blend_mode = b->m_pattern_blend_mode,
.offset = Canvas::I->m_pattern_offset,
},
.mvp = plane_mvp_z,
.layer_alpha = 1.0f,
.alpha_lock = m_canvas->m_layers[layer_index]->m_alpha_locked,
.mask_enabled = m_canvas->m_smask_active,
.use_fragcoord = false,
.blend_mode = b->m_blend_mode,
.use_dual = b->m_dual_enabled,
.dual_blend_mode = b->m_dual_blend_mode,
.dual_alpha = b->m_dual_opacity,
.use_pattern = b->m_pattern_enabled && !b->m_pattern_eachsample,
},
.layer_texture = {
.mvp = plane_mvp_z,
.texture_slot = 0,
.alpha = 1.f,
.highlight = m_canvas->m_layers[layer_index]->m_hightlight,
},
.blend = {
.shader = {
.mvp = glm::ortho(-1, 1, -1, 1),
.texture_slot = 0,
.destination_texture_slot = 2,
.use_destination_texture = copy_blend_destination,
.blend_mode = m_canvas->m_layers[layer_index]->m_blend_mode,
.alpha = 1.f,
},
.copy_destination = copy_blend_destination,
},
.use_nearest_sampler = m_canvas->m_cam_fov < 20.f,
.use_dual_texture = b->m_dual_enabled,
},
{
.bind_blender_framebuffer = [&] {
m_blender_rtt.bindFramebuffer();
},
.clear_blender_framebuffer = [&] {
m_blender_rtt.clear();
},
.unbind_blender_framebuffer = [&] {
m_blender_rtt.unbindFramebuffer();
},
.bind_sampler = [&](int unit) {
m_sampler.bind(unit);
},
.bind_nearest_sampler = [&](int unit) {
m_sampler_nearest.bind(unit);
},
.bind_stencil_sampler = [&](int unit) {
m_sampler_stencil.bind(unit);
},
.set_active_texture_unit = [&](int unit) {
set_active_texture_unit(unit);
},
.bind_temporary_texture = [&] {
m_canvas->m_tmp[plane_index].bindTexture();
},
.unbind_temporary_texture = [&] {
m_canvas->m_tmp[plane_index].unbindTexture();
},
.bind_smask_texture = [&] {
m_canvas->m_smask.rtt(plane_index).bindTexture();
},
.unbind_smask_texture = [&] {
m_canvas->m_smask.rtt(plane_index).unbindTexture();
},
.bind_temporary_dual_texture = [&] {
m_canvas->m_tmp_dual[plane_index].bindTexture();
},
.unbind_temporary_dual_texture = [&] {
m_canvas->m_tmp_dual[plane_index].unbindTexture();
},
.bind_pattern_texture = [&] {
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
unbind_texture_2d();
},
.draw_face = [&] {
m_face_plane.draw_fill();
},
.bind_blender_texture = [&] {
m_blender_rtt.bindTexture();
},
.unbind_blender_texture = [&] {
m_blender_rtt.unbindTexture();
},
.bind_destination_texture = [&] {
m_blender_bg.bind();
},
.unbind_destination_texture = [&] {
m_blender_bg.unbind();
},
.copy_destination_framebuffer = [&] {
copy_framebuffer_to_texture_2d(
0,
0,
0,
0,
m_blender_bg.size().x,
m_blender_bg.size().y);
},
.draw_debug_outline =
#ifdef _DEBUG
[&] {
auto bb = m_canvas->m_layers[layer_index]->box(plane_index) / (float)m_canvas->m_layers[layer_index]->w;
glm::vec2 bbmin = xy(bb);
glm::vec2 bbsz = zw(bb) - xy(bb);
pp::panopainter::configure_legacy_ui_color_shader(
plane_mvp_z
* glm::translate(glm::vec3(bbmin * 2.f, 0))
* glm::translate(glm::vec3(-1, -1, 0))
* glm::scale(glm::vec3(bbsz, 1))
* glm::translate(glm::vec3(1, 1, 0)),
{ 1, 0, 0, 1 });
m_face_plane.draw_stroke();
},
#else
[] {
},
#endif
.draw_frame = draw_layer_frame,
});
plane_mvp_z,
b.get(),
copy_blend_destination,
m_canvas->m_cam_fov < 20.f);
pp::panopainter::execute_legacy_canvas_draw_merge_layer_path(
m_canvas->m_current_stroke && m_canvas->m_current_mode == kCanvasMode::Erase && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index,