Advance app runtime ownership and modernization docs

This commit is contained in:
2026-06-16 06:35:59 +02:00
parent c3d757f4a4
commit a76560e3df
24 changed files with 6675 additions and 9009 deletions

View File

@@ -30,11 +30,12 @@ Read these first:
- Commit and push each verified successful progress slice.
- Prefer larger coherent slices over tiny checkpoints, but keep docs/debt updated
with each slice.
- After a verified slice is committed and pushed, reset conversation context
before starting the next slice when practical, especially if the thread is
approaching automatic compaction. Record all needed resume state in committed
code/docs first so the next thread can restart from `AGENTS.md`, roadmap/debt,
and git history instead of relying on chat transcript context.
- Treat automatic compaction as a failure mode to avoid. Keep active context
small, commit and push before the thread grows large, and reset conversation
context between verified slices when practical instead of carrying excess
history forward. Record all needed resume state in committed code/docs first
so the next thread can restart from `AGENTS.md`, roadmap/debt, and git
history instead of relying on chat transcript context.
- Do not revert user changes. Unrelated untracked notes, such as
`docs/human-review-notes.md`, should be left alone unless explicitly requested.
- Use CMake as the source of truth. Legacy Visual Studio project files are not the
@@ -42,8 +43,9 @@ Read these first:
- Use `apply_patch` for manual source/doc edits.
- For delegated work, follow `docs/modernization/director-workflow.md`: the
coordinator keeps integration locally, assigns direct worker tasks, uses
`gpt-5.3-codex-spark` workers by default, and gives them the exact project
context needed so they do not spend tokens re-reading repo docs.
`gpt-5.4-mini` workers by default, and gives them a minimal task packet with
only the build, test, and code-exploration context needed so they do not
spend tokens re-reading repo docs.
## Build And Test

View File

@@ -86,6 +86,7 @@ set(PP_PLATFORM_LINUX_SOURCES
set(PP_PANOPAINTER_APP_SOURCES
src/app.cpp
src/app_runtime.cpp
src/app_cloud.cpp
src/app_commands.cpp
src/app_dialogs.cpp

View File

@@ -1,7 +1,7 @@
# PanoPainter Capability Map
Status: live
Last updated: 2026-06-05
Last updated: 2026-06-16
This map is the preservation checklist for the modernization. When a component
is extracted, update the relevant rows with the owning component, test label,
@@ -59,6 +59,7 @@ and validation command.
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch, layout, ownership-handle, callback-disconnect, and destroy-during-callback tests |
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui`, `pp_ui_core` | UI automation scripts, command-dispatch view models, pure overlay lifetime tests, retained overlay-adapter build coverage, retained popup/dialog lifetime tests |
| UI ownership and thread affinity | `Node`, `LayoutManager`, `App` UI queue, retained callbacks | `pp_ui_core`, app runtime service, `pp_panopainter_ui` | Checked-handle dispatch, scoped callback disconnect, destroy-during-callback, close-during-dispatch, and UI-thread post/drain/shutdown coverage |
| Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation |
| Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings |
@@ -67,6 +68,7 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback |
| Render/UI task dispatch and worker shutdown | `App`, `Canvas`, retained worker threads, platform entrypoints | app runtime service, `pp_foundation`, `pp_platform_*` | Render/UI queue order, same-thread dispatch, cancellation, shutdown drain, and no-detached-worker ownership coverage |
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service |
| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke |

View File

@@ -335,6 +335,10 @@ agent or engineer to remove them without reconstructing context from chat.
routes the stroke preset/brush/dual-brush/pattern popup open-close flow through
checked overlay handles and explicit partial-open cleanup; migration is still
pending in remaining panel families.
- 2026-06-16: `DEBT-0063` was narrowed again. `src/node_panel_stroke.cpp` now
routes stroke popup close/reset through a shared overlay helper, and
`src/node_popup_menu.cpp` now keeps the popup alive through selection dispatch
with scoped ownership instead of raw `this` callbacks.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_brush.cpp` now
routes brush popup-menu open-close flow through checked overlay handles and
handle-based close on selection.

View File

@@ -1,7 +1,7 @@
# Modernization Coordinator Workflow
Status: live
Last updated: 2026-06-14
Last updated: 2026-06-16
Use this workflow when the user explicitly asks for subagents, delegation, a
coordinator, or parallel agent work. Do not spawn subagents just because a task
@@ -16,10 +16,16 @@ layer.
- Save main-thread tokens by keeping implementation and focused lookup out of
the coordinator context.
- Treat thread compaction as wasted budget. Prefer small active context,
committed resume state, and fresh follow-on threads over carrying long stale
history.
- Keep each implementation slice measurable, validated, committed, and pushed.
- Avoid merge conflicts by giving every worker a disjoint task and file scope.
- Keep workers stateless between tasks: when a worker finishes, integrate or
reject the result, then clear that worker context before the next assignment.
- Do not leave workers idle. Either give a worker another coherent follow-on
task while its context is still useful, or close it promptly.
- Reuse worker context only for closely related follow-on tasks in the same
local area; otherwise close the worker and start a fresh one with a new
minimal packet.
- Keep prompts dense with the exact project/task context workers need so they do
not spend tokens re-reading repo docs or re-parsing broad areas.
- Keep communication terse: no fillers, no cheerleading, no narrative padding.
@@ -40,7 +46,8 @@ The coordinator is the main agent in the user thread. The coordinator owns:
- running final validation
- updating docs/debt/tasks
- committing and pushing the verified slice
- resetting worker context after each completed or rejected task
- either assigning coherent follow-on work to an active worker or closing that
worker once its useful context window is over
The coordinator should keep local work minimal. It may do a quick blocking
check before delegation, such as reading task rows, checking git status, or
@@ -49,13 +56,16 @@ code or test work, delegate it directly to a worker whenever the scope can be
made clear.
The coordinator must front-load context. Workers should not be told to "read
the roadmap" or "inspect the repo" unless that is the task. The coordinator is
responsible for extracting and passing:
the roadmap", "read AGENTS.md", or "inspect the repo" unless that is the task.
The coordinator is responsible for extracting and passing:
- task ids and done checks
- debt ids and removal conditions that matter
- exact write scope and allowed read scope
- required validation commands
- the specific build/test preset or target names the worker may need
- the exact code-exploration tools to use for the slice, such as `rg` or the
compiler-aware `clangd_nav.py` helper
- relevant file paths, code references, and current behavior notes
- any repo rules or user constraints that materially affect the task
@@ -68,6 +78,11 @@ Workers do not own repo discovery. They start from the coordinator-provided
context packet and stay inside the assigned scope unless they hit a blocker that
requires a narrow follow-up question.
Workers may be kept alive for more than one assignment only when the next task
is coherent with the current one: same subsystem, overlapping read scope,
similar validation path, and no avoidable context rebuild. Do not keep workers
alive just because a slot is available.
Every worker and explorer must be told:
- this repository may have other agents working in parallel
@@ -80,13 +95,13 @@ Every worker and explorer must be told:
| Work Type | Model | Reasoning Effort | Use |
| --- | --- | --- | --- |
| Coordinator orchestration and integration | inherited main-thread model or `gpt-5.4-mini` | `low` or `medium` | Scope selection, task routing, conflict checks, validation, docs/debt updates, commits, pushes. |
| Direct worker coding task | `gpt-5.3-codex-spark` | `medium` | Bounded implementation in known files with coordinator-supplied context. |
| Direct worker lookup or inventory | `gpt-5.3-codex-spark` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
| Mechanical docs cleanup | `gpt-5.3-codex-spark` | `low` | Formatting, table updates, command normalization. |
| Coordinator orchestration and integration | `gpt-5.4` | `low` or `medium` | Scope selection, task routing, conflict checks, validation, docs/debt updates, commits, pushes, and worker packet preparation. |
| Direct worker coding task | `gpt-5.4-mini` | `medium` | Bounded implementation in known files with coordinator-supplied context. |
| Direct worker lookup or inventory | `gpt-5.4-mini` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
| Mechanical docs cleanup | `gpt-5.4-mini` | `low` | Formatting, table updates, command normalization. |
| Coordinator-only escalation | inherited higher model only when explicitly justified | inherited | Resolve architecture ambiguity, conflict integration, or task decomposition failures without adding a captain layer. |
Workers default to `gpt-5.3-codex-spark`. If a task looks too broad or risky
Workers default to `gpt-5.4-mini`. If a task looks too broad or risky
for that model, the coordinator should decompose it further or keep the narrow
integration step locally instead of inserting an extra management tier.
@@ -96,33 +111,46 @@ integration step locally instead of inserting an extra management tier.
ids, validation commands, and only the necessary excerpts.
- Use `fork_context=true` only when prior conversation details are essential and
not already captured in the worker prompt.
- Do not let the coordinator thread drift toward compaction. Once a verified
slice is committed and pushed, prefer a fresh thread for the next slice if
the remaining context is no longer tight.
- Do not paste large logs into prompts. Point workers at log paths and ask for
the smallest relevant excerpt.
- Do not ask workers to broadly read `AGENTS.md`, the roadmap, or the debt log.
Summarize the exact rules and rows they need.
- Do not ask workers to broadly read `AGENTS.md`, the roadmap, the debt log, or
other repo-wide docs. Summarize the exact rules and rows they need.
- Default worker context to a minimal operating packet: task id, assigned file
scope, build command, test command, and code-exploration command hints.
- Keep worker prompts compact but complete. Shorter is good only if it still
removes the need for worker-side repo rediscovery.
- Ask for compact final reports: changed files, result, validation, blockers,
next recommendation.
- Close completed workers after their results are integrated or rejected.
- Keep active workers busy with another coherent task when that is cheaper than
restarting context; otherwise close them immediately after integration.
- Close workers that are done, blocked, or no longer have a strong context
advantage so they do not accumulate and saturate worker slots.
- Prefer the smallest number of concurrent workers that keeps disjoint work
moving.
- Use rolling integration: wait for whichever worker finishes first, process the
result, then launch the next disjoint worker with a fresh context window.
result, then either reuse that worker for the next coherent slice or close it
before launching a fresh worker for unrelated work.
- Prefer committed repo state over chat history as the handoff mechanism between
slices so worker and coordinator prompts stay short.
## Delegation Flow
1. Coordinator picks one or more `Ready` tasks from
`docs/modernization/tasks.md` with disjoint write scopes.
2. Coordinator splits each task into direct worker-sized units.
2. Coordinator splits each task into direct worker-sized units, grouping
coherent follow-on work when one worker can finish it efficiently without
broadening scope.
3. Coordinator prepares a context packet for each worker with the exact task
requirements, file scope, validation commands, and relevant project details.
4. Coordinator assigns the task directly to a `gpt-5.3-codex-spark` worker or
4. Coordinator assigns the task directly to a `gpt-5.4-mini` worker or
explorer.
5. Worker returns changed files, validation, blockers, and any narrow
integration notes.
6. Coordinator reviews for scope conflicts, integrates the result, and clears
that worker context before assigning the next task.
6. Coordinator reviews for scope conflicts, integrates the result, and decides
whether to give that same worker another coherent task or close it.
7. Coordinator runs the listed validation command or the quiet checkpoint
wrapper for each integrated slice.
8. Coordinator updates `tasks.md`, `debt.md`, and `roadmap.md` if task state or
@@ -132,7 +160,7 @@ integration step locally instead of inserting an extra management tier.
## Coordinator Prompt Template For A Worker
```text
You are a `gpt-5.3-codex-spark` worker on PanoPainter. Other agents may be
You are a `gpt-5.4-mini` worker on PanoPainter. Other agents may be
editing nearby files; do not revert unrelated changes.
Task source: docs/modernization/tasks.md task(s) <TASK-ID-LIST>.
@@ -142,15 +170,24 @@ Debt ids: <DEBT-LIST>.
Write scope: <FILES/DIRS ONLY>.
Read scope: <FILES/DIRS>.
Validation: <COMMANDS>.
Code exploration: <RG OR CLANGD_NAV COMMANDS TO USE>.
Repo constraints you must follow:
- <ONLY THE RELEVANT RULES>
Current context you should rely on instead of broad repo/doc review:
Minimal context you should rely on instead of broad repo/doc review:
- <CURRENT BEHAVIOR NOTE>
- <RELEVANT FILE OR SYMBOL NOTE>
- <WHY THIS SLICE IS SAFE / WHAT MUST NOT CHANGE>
Do not read repo-wide docs unless this task explicitly requires it. Use only
the supplied context, the listed file scope, and the build/test/code-exploration
commands above.
If the coordinator gives you a second task, accept it only when it is coherent
with the current scope and does not require broad repo rediscovery. Otherwise
say that a fresh worker should be used.
Make the smallest behavior-preserving change that satisfies the done checks.
Do not spend tokens on broad document review or inventory outside the assigned
scope unless the task explicitly requires it. If the task is larger than
@@ -187,6 +224,11 @@ Return only:
- Focused validation for the task passed or the failure is documented.
- `docs/modernization/debt.md` changed when debt was narrowed or closed.
- `docs/modernization/tasks.md` score changed only for `Done` tasks.
- Each worker result was integrated before that worker context was reused.
- Each worker result was integrated before that worker was reused.
- Reused workers only handled coherent follow-on tasks with a real context
advantage.
- Done or blocked workers were closed instead of being left idle.
- The coordinator did not carry unnecessary stale history when a fresh thread
would have been cheaper than compaction.
- The commit contains one coherent slice.
- The branch was pushed.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -32,10 +32,6 @@
App* App::I = nullptr; // singleton
std::deque<AppTask> App::render_tasklist;
std::mutex App::render_task_mutex;
std::condition_variable App::render_cv;
namespace {
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
@@ -130,17 +126,6 @@ void apply_app_scissor_test(bool enabled)
}
}
std::thread App::render_thread;
std::thread::id App::render_thread_id;
bool App::render_running = false;
std::deque<AppTask> App::ui_tasklist;
std::mutex App::ui_task_mutex;
std::condition_variable App::ui_cv;
std::thread App::ui_thread;
std::thread::id App::ui_thread_id;
bool App::ui_running = false;
void App::create()
{
const auto initial_surface = pp::app::plan_app_initial_surface();
@@ -353,7 +338,7 @@ void App::async_redraw()
if (plan.set_redraw)
redraw = true;
if (plan.notify_ui)
ui_cv.notify_all();
runtime_.notify_ui_worker();
}
void App::async_end()
@@ -699,252 +684,40 @@ void App::rec_loop()
void App::render_thread_tick()
{
static uint32_t count = 0;
render_thread_id = std::this_thread::get_id();
std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
// move the task list locally to free the queue for other threads
{
std::unique_lock<std::mutex> lock(render_task_mutex);
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size());
render_running = drain_plan.mark_running;
if (!drain_plan.drain_tasks)
return;
working_list = std::move(render_tasklist);
}
// execute the tasks
if (drain_plan.wrap_in_render_context)
{
async_start();
while (!working_list.empty())
{
//LOG("render task %d", count);
//LOG("task %s", working_list.front().name.c_str());
count++;
working_list.front()();
working_list.pop_front();
}
async_end();
}
runtime_.render_thread_tick(*this);
}
void App::render_thread_main()
{
BT_SetTerminate();
uint32_t count = 0;
render_thread_id = std::this_thread::get_id();
render_running = pp::app::plan_app_thread_start().mark_running;
while (render_running)
{
std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
// move the task list locally to free the queue for other threads
{
std::unique_lock<std::mutex> lock(render_task_mutex);
render_cv.wait(lock, [this] { return render_tasklist.empty() && render_running ? false : true; });
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size());
working_list = std::move(render_tasklist);
}
// execute the tasks
if (drain_plan.wrap_in_render_context)
{
async_start();
while (!working_list.empty())
{
//LOG("render task %d", count);
//LOG("task %s", working_list.front().name.c_str());
count++;
working_list.front()();
working_list.pop_front();
}
async_end();
}
}
runtime_.render_thread_main(*this);
}
void App::ui_thread_tick()
{
ui_thread_id = std::this_thread::get_id();
std::deque<AppTask> working_list;
pp::app::AppUiTickPlan tick_plan;
// move the task list locally to free the queue for other threads
{
std::unique_lock<std::mutex> lock(ui_task_mutex);
tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist.size(), redraw);
ui_running = tick_plan.mark_running;
working_list = std::move(ui_tasklist);
}
// execute the tasks
if (tick_plan.execute_tasks)
{
while (!working_list.empty())
{
//LOG("ui task %d", count);
working_list.front()();
working_list.pop_front();
}
}
if (tick_plan.tick_app)
tick(0);
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, 0);
if (redraw_plan.enqueue_render_frame)
{
if (redraw_plan.update_before_render)
update(0);
render_task([this]
{
bind_default_render_target();
clear();
draw(0);
async_swap();
});
}
runtime_.ui_thread_tick(*this);
}
void App::ui_thread_main()
{
BT_SetTerminate();
uint32_t count = 0;
ui_thread_id = std::this_thread::get_id();
ui_running = pp::app::plan_app_thread_start().mark_running;
attach_ui_thread();
LOG("ui thread init()");
init();
auto t_start = std::chrono::high_resolution_clock::now();
float t_frame = 0;
float t_fps_counter = 0;
float t_reloader = 0;
int rendered_frames = 0;
while (ui_running)
{
std::deque<AppTask> working_list;
// move the task list locally to free the queue for other threads
{
std::unique_lock<std::mutex> lock(ui_task_mutex);
ui_cv.wait_for(lock, std::chrono::milliseconds(idle_ms),
[this] { return ui_tasklist.empty() && ui_running ? false : true; });
working_list = std::move(ui_tasklist);
}
// execute the tasks
if (!working_list.empty())
{
while (!working_list.empty())
{
//LOG("ui task %d", count);
count++;
working_list.front()();
working_list.pop_front();
}
}
auto t_now = std::chrono::high_resolution_clock::now();
float dt = std::chrono::duration<float>(t_now - t_start).count();
t_start = t_now;
const auto timer_plan = pp::app::plan_app_ui_loop_timers(
dt,
t_frame,
t_fps_counter,
t_reloader,
rendered_frames,
platform_enables_live_asset_reloading());
if (timer_plan) {
if (timer_plan.value().update_platform_frame)
update_platform_frame(dt);
t_frame = timer_plan.value().frame_accumulator;
t_fps_counter = timer_plan.value().fps_accumulator;
t_reloader = timer_plan.value().reload_accumulator;
rendered_frames = timer_plan.value().rendered_frames_after_report;
if (timer_plan.value().report_rendered_frames)
report_rendered_frames(timer_plan.value().reported_frame_count);
if (timer_plan.value().check_live_asset_reload) {
if (ShaderManager::reload())
{
stroke->update_controls();
redraw = true;
}
if (layout.reload())
redraw = true;
if (layout_designer.reload())
redraw = true;
}
}
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, rendered_frames);
if (redraw_plan.tick_app)
tick(dt);
if (redraw_plan.enqueue_render_frame)
{
if (redraw_plan.update_before_render)
update(t_frame);
render_task([this, t_frame]
{
bind_default_render_target();
clear();
draw(t_frame);
async_swap();
});
if (redraw_plan.reset_frame_accumulator)
t_frame = 0;
rendered_frames = redraw_plan.rendered_frames;
}
}
detach_ui_thread();
runtime_.ui_thread_main(*this);
}
void App::render_thread_start()
{
const auto plan = pp::app::plan_app_thread_start();
if (plan.start_thread)
render_thread = std::thread(&App::render_thread_main, this);
render_running = plan.mark_running;
runtime_.render_thread_start(*this);
}
void App::render_thread_stop()
{
const auto plan = pp::app::plan_app_thread_stop(render_thread.joinable());
if (plan.mark_not_running)
render_running = false;
if (plan.notify_worker)
render_cv.notify_all();
if (plan.join_thread)
render_thread.join();
runtime_.render_thread_stop();
}
void App::ui_thread_start()
{
const auto plan = pp::app::plan_app_thread_start();
if (plan.start_thread)
ui_thread = std::thread(&App::ui_thread_main, this);
ui_running = plan.mark_running;
runtime_.ui_thread_start(*this);
}
void App::ui_thread_stop()
{
const auto plan = pp::app::plan_app_thread_stop(ui_thread.joinable());
if (plan.mark_not_running)
ui_running = false;
if (plan.notify_worker)
ui_cv.notify_all();
if (plan.join_thread)
ui_thread.join();
runtime_.ui_thread_stop();
}

169
src/app.h
View File

@@ -25,6 +25,7 @@
#include "layout.h"
#include "app_core/document_session.h"
#include "app_core/app_thread.h"
#include "app_runtime.h"
namespace pp::platform {
class PlatformServices;
@@ -76,21 +77,6 @@ struct VRController
virtual float get_trigger_value() const { return 1.f; }
};
struct AppTask : public std::packaged_task<void()>
{
size_t task_id;
#ifdef _DEBUG
std::string name;
#endif
template<typename F> AppTask(F f) : std::packaged_task<void()>(f)
{
task_id = typeid(f).hash_code();
#ifdef _DEBUG
name = typeid(f).name();
#endif
}
};
class App
{
public:
@@ -345,16 +331,13 @@ public:
void cmd_convert(std::string pano_path, std::string out_path);
AppRuntime& runtime() noexcept { return runtime_; }
const AppRuntime& runtime() const noexcept { return runtime_; }
//////////////////////////////////////////////////////////////////////////
// RENDER THREAD
//////////////////////////////////////////////////////////////////////////
static std::deque<AppTask> render_tasklist;
static std::mutex render_task_mutex;
static std::condition_variable render_cv;
static std::thread render_thread;
static std::thread::id render_thread_id;
static bool render_running;
void render_thread_tick();
void render_thread_main();
void render_thread_start();
@@ -362,94 +345,30 @@ public:
bool is_render_thread()
{
return std::this_thread::get_id() == render_thread_id;
return runtime_.is_render_thread();
}
// don't capture a reference to this ptr as the object may be destroyed
// by the time the task is executed
template<typename T>
std::future<void> render_task_async(T task, bool unique = false)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
unique,
0U,
render_running,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
render_tasklist.size(),
render_running,
false,
false);
// remove any previously queued task from the same lambda
if (queue_dispatch.remove_matching_unique_task)
render_tasklist.erase(std::remove_if(render_tasklist.begin(), render_tasklist.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist.end());
render_tasklist.push_back(std::move(pt));
}
if (dispatch.notify_worker)
render_cv.notify_all();
}
return f;
return runtime_.render_task_async(std::move(task), unique);
}
template<typename T>
void render_task(T task)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
false,
0U,
render_running,
true,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex);
render_tasklist.push_back(std::move(pt));
}
if (dispatch.notify_worker)
render_cv.notify_all();
}
if (dispatch.wait_for_completion)
f.get();
runtime_.render_task(std::move(task));
}
void render_sync()
{
render_task([] {});
runtime_.render_sync();
}
//////////////////////////////////////////////////////////////////////////
// UI THREAD
//////////////////////////////////////////////////////////////////////////
static std::deque<AppTask> ui_tasklist;
static std::mutex ui_task_mutex;
static std::condition_variable ui_cv;
static std::thread ui_thread;
static std::thread::id ui_thread_id;
static bool ui_running;
void ui_thread_tick();
void ui_thread_main();
void ui_thread_start();
@@ -457,83 +376,29 @@ public:
bool is_ui_thread()
{
return std::this_thread::get_id() == ui_thread_id;
return runtime_.is_ui_thread();
}
// don't capture a reference to this ptr as the object may be destroyed
// by the time the task is executed
template<typename T>
std::future<void> ui_task_async(T task, bool unique = false)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
unique,
0U,
ui_running,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
ui_tasklist.size(),
ui_running,
false,
false);
// remove any previously queued task from the same lambda
if (queue_dispatch.remove_matching_unique_task)
ui_tasklist.erase(std::remove_if(ui_tasklist.begin(), ui_tasklist.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist.end());
ui_tasklist.push_back(std::move(pt));
}
if (dispatch.notify_worker)
ui_cv.notify_all();
}
return f;
return runtime_.ui_task_async(std::move(task), unique);
}
template<typename T>
void ui_task(T task)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
false,
0U,
ui_running,
true,
true);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex);
ui_tasklist.push_back(std::move(pt));
}
if (dispatch.notify_worker)
ui_cv.notify_all();
}
if (dispatch.wait_for_completion)
f.get();
if (dispatch.request_redraw)
runtime_.ui_task(std::move(task));
if (runtime_.request_redraw())
redraw = true;
runtime_.clear_request_redraw();
}
void ui_sync()
{
ui_task([] {});
runtime_.ui_sync();
}
private:
AppRuntime runtime_;
};

View File

@@ -19,7 +19,7 @@ void App::cloud_upload()
void App::cloud_upload_all()
{
std::thread([this] {
pp::panopainter::queue_legacy_cloud_worker_task([this] {
BT_SetTerminate();
auto names = Asset::list_files(data_path, ".*\\.ppi");
@@ -28,7 +28,7 @@ void App::cloud_upload_all()
const auto status = pp::panopainter::execute_legacy_cloud_bulk_upload_plan(*this, plan);
if (!status.ok())
LOG("Cloud bulk upload action failed: %s", status.message);
}).detach();
});
}
void App::cloud_browse()

258
src/app_runtime.cpp Normal file
View File

@@ -0,0 +1,258 @@
#include "pch.h"
#include "app_runtime.h"
#include "app.h"
bool AppRuntime::is_render_thread() const noexcept
{
return std::this_thread::get_id() == render_thread_id_;
}
bool AppRuntime::is_ui_thread() const noexcept
{
return std::this_thread::get_id() == ui_thread_id_;
}
void AppRuntime::notify_render_worker() noexcept
{
render_cv_.notify_all();
}
void AppRuntime::notify_ui_worker() noexcept
{
ui_cv_.notify_all();
}
void AppRuntime::render_thread_tick(App& app)
{
static uint32_t count = 0;
render_thread_id_ = std::this_thread::get_id();
std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
{
std::unique_lock<std::mutex> lock(render_task_mutex_);
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
render_running_ = drain_plan.mark_running;
if (!drain_plan.drain_tasks)
return;
working_list = std::move(render_tasklist_);
}
if (drain_plan.wrap_in_render_context)
{
app.async_start();
while (!working_list.empty())
{
count++;
working_list.front()();
working_list.pop_front();
}
app.async_end();
}
}
void AppRuntime::render_thread_main(App& app)
{
BT_SetTerminate();
render_thread_id_ = std::this_thread::get_id();
render_running_ = pp::app::plan_app_thread_start().mark_running;
while (render_running_)
{
std::deque<AppTask> working_list;
pp::app::AppQueueDrainPlan drain_plan;
{
std::unique_lock<std::mutex> lock(render_task_mutex_);
render_cv_.wait(lock, [this] { return render_tasklist_.empty() && render_running_ ? false : true; });
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
working_list = std::move(render_tasklist_);
}
if (drain_plan.wrap_in_render_context)
{
app.async_start();
while (!working_list.empty())
{
working_list.front()();
working_list.pop_front();
}
app.async_end();
}
}
}
void AppRuntime::ui_thread_tick(App& app)
{
ui_thread_id_ = std::this_thread::get_id();
std::deque<AppTask> working_list;
pp::app::AppUiTickPlan tick_plan;
{
std::unique_lock<std::mutex> lock(ui_task_mutex_);
tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist_.size(), app.redraw);
ui_running_ = tick_plan.mark_running;
working_list = std::move(ui_tasklist_);
}
if (tick_plan.execute_tasks)
{
while (!working_list.empty())
{
working_list.front()();
working_list.pop_front();
}
}
if (tick_plan.tick_app)
app.tick(0);
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(app.redraw, 0);
if (redraw_plan.enqueue_render_frame)
{
if (redraw_plan.update_before_render)
app.update(0);
app.render_task([&app]
{
app.bind_default_render_target();
app.clear();
app.draw(0);
app.async_swap();
});
}
}
void AppRuntime::ui_thread_main(App& app)
{
BT_SetTerminate();
ui_thread_id_ = std::this_thread::get_id();
ui_running_ = pp::app::plan_app_thread_start().mark_running;
app.attach_ui_thread();
LOG("ui thread init()");
app.init();
auto t_start = std::chrono::high_resolution_clock::now();
float t_frame = 0;
float t_fps_counter = 0;
float t_reloader = 0;
int rendered_frames = 0;
while (ui_running_)
{
std::deque<AppTask> working_list;
{
std::unique_lock<std::mutex> lock(ui_task_mutex_);
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms),
[this] { return ui_tasklist_.empty() && ui_running_ ? false : true; });
working_list = std::move(ui_tasklist_);
}
if (!working_list.empty())
{
while (!working_list.empty())
{
working_list.front()();
working_list.pop_front();
}
}
auto t_now = std::chrono::high_resolution_clock::now();
float dt = std::chrono::duration<float>(t_now - t_start).count();
t_start = t_now;
const auto timer_plan = pp::app::plan_app_ui_loop_timers(
dt,
t_frame,
t_fps_counter,
t_reloader,
rendered_frames,
app.platform_enables_live_asset_reloading());
if (timer_plan) {
if (timer_plan.value().update_platform_frame)
app.update_platform_frame(dt);
t_frame = timer_plan.value().frame_accumulator;
t_fps_counter = timer_plan.value().fps_accumulator;
t_reloader = timer_plan.value().reload_accumulator;
rendered_frames = timer_plan.value().rendered_frames_after_report;
if (timer_plan.value().report_rendered_frames)
app.report_rendered_frames(timer_plan.value().reported_frame_count);
if (timer_plan.value().check_live_asset_reload) {
if (ShaderManager::reload())
{
app.stroke->update_controls();
app.redraw = true;
}
if (app.layout.reload())
app.redraw = true;
if (app.layout_designer.reload())
app.redraw = true;
}
}
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(app.redraw, rendered_frames);
if (redraw_plan.tick_app)
app.tick(dt);
if (redraw_plan.enqueue_render_frame)
{
if (redraw_plan.update_before_render)
app.update(t_frame);
app.render_task([&app, t_frame]
{
app.bind_default_render_target();
app.clear();
app.draw(t_frame);
app.async_swap();
});
if (redraw_plan.reset_frame_accumulator)
t_frame = 0;
rendered_frames = redraw_plan.rendered_frames;
}
}
app.detach_ui_thread();
}
void AppRuntime::render_thread_start(App& app)
{
const auto plan = pp::app::plan_app_thread_start();
if (plan.start_thread)
render_thread_ = std::thread(&AppRuntime::render_thread_main, this, std::ref(app));
render_running_ = plan.mark_running;
}
void AppRuntime::render_thread_stop()
{
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
if (plan.mark_not_running)
render_running_ = false;
if (plan.notify_worker)
render_cv_.notify_all();
if (plan.join_thread)
render_thread_.join();
}
void AppRuntime::ui_thread_start(App& app)
{
const auto plan = pp::app::plan_app_thread_start();
if (plan.start_thread)
ui_thread_ = std::thread(&AppRuntime::ui_thread_main, this, std::ref(app));
ui_running_ = plan.mark_running;
}
void AppRuntime::ui_thread_stop()
{
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
if (plan.mark_not_running)
ui_running_ = false;
if (plan.notify_worker)
ui_cv_.notify_all();
if (plan.join_thread)
ui_thread_.join();
}

216
src/app_runtime.h Normal file
View File

@@ -0,0 +1,216 @@
#pragma once
#include "app_core/app_thread.h"
#include <algorithm>
#include <condition_variable>
#include <cstddef>
#include <deque>
#include <functional>
#include <future>
#include <mutex>
#include <string>
#include <typeinfo>
#include <utility>
#include <thread>
class App;
struct AppTask : public std::packaged_task<void()>
{
size_t task_id;
#ifdef _DEBUG
std::string name;
#endif
template<typename F> AppTask(F f) : std::packaged_task<void()>(f)
{
task_id = typeid(f).hash_code();
#ifdef _DEBUG
name = typeid(f).name();
#endif
}
};
class AppRuntime
{
public:
[[nodiscard]] bool is_render_thread() const noexcept;
[[nodiscard]] bool is_ui_thread() const noexcept;
[[nodiscard]] bool request_redraw() const noexcept { return request_redraw_; }
void clear_request_redraw() noexcept { request_redraw_ = false; }
void notify_render_worker() noexcept;
void notify_ui_worker() noexcept;
void render_thread_tick(App& app);
void render_thread_main(App& app);
void render_thread_start(App& app);
void render_thread_stop();
void ui_thread_tick(App& app);
void ui_thread_main(App& app);
void ui_thread_start(App& app);
void ui_thread_stop();
template<typename T>
std::future<void> render_task_async(T task, bool unique = false)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
unique,
0U,
render_running_,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
render_tasklist_.size(),
render_running_,
false,
false);
if (queue_dispatch.remove_matching_unique_task)
render_tasklist_.erase(std::remove_if(render_tasklist_.begin(), render_tasklist_.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist_.end());
render_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
render_cv_.notify_all();
}
return f;
}
template<typename T>
void render_task(T task)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_render_thread(),
false,
0U,
render_running_,
true,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(render_task_mutex_);
render_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
render_cv_.notify_all();
}
if (dispatch.wait_for_completion)
f.get();
}
void render_sync()
{
render_task([] {});
}
template<typename T>
std::future<void> ui_task_async(T task, bool unique = false)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
unique,
0U,
ui_running_,
false,
false);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
false,
unique,
ui_tasklist_.size(),
ui_running_,
false,
false);
if (queue_dispatch.remove_matching_unique_task)
ui_tasklist_.erase(std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(),
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist_.end());
ui_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
ui_cv_.notify_all();
}
return f;
}
template<typename T>
void ui_task(T task)
{
AppTask pt(task);
auto f = pt.get_future();
const auto dispatch = pp::app::plan_app_task_dispatch(
is_ui_thread(),
false,
0U,
ui_running_,
true,
true);
if (dispatch.execute_immediately)
{
pt();
}
else if (dispatch.queue_task)
{
{
std::lock_guard<std::mutex> lock(ui_task_mutex_);
ui_tasklist_.push_back(std::move(pt));
}
if (dispatch.notify_worker)
ui_cv_.notify_all();
}
if (dispatch.wait_for_completion)
f.get();
if (dispatch.request_redraw)
request_redraw_ = true;
}
void ui_sync()
{
ui_task([] {});
}
private:
std::deque<AppTask> render_tasklist_;
std::mutex render_task_mutex_;
std::condition_variable render_cv_;
std::thread render_thread_;
std::thread::id render_thread_id_;
bool render_running_ = false;
std::deque<AppTask> ui_tasklist_;
std::mutex ui_task_mutex_;
std::condition_variable ui_cv_;
std::thread ui_thread_;
std::thread::id ui_thread_id_;
bool ui_running_ = false;
bool request_redraw_ = false;
};

View File

@@ -21,6 +21,79 @@ pp::foundation::Status execute_legacy_cloud_download_selection_action(
namespace {
class LegacyCloudWorker final {
public:
LegacyCloudWorker()
: worker_([this](std::stop_token stop_token) {
run(stop_token);
})
{
}
~LegacyCloudWorker()
{
shutdown();
}
void post(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_)
return;
tasks_.push_back(std::move(task));
}
cv_.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
}
void run(std::stop_token stop_token)
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [&] {
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
});
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
break;
task = std::move(tasks_.front());
tasks_.pop_front();
}
if (task) {
try {
task();
} catch (...) {
LOG("cloud worker task failed");
}
}
}
}
std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::function<void()>> tasks_;
bool stopping_ = false;
std::jthread worker_;
};
LegacyCloudWorker& cloud_worker()
{
static LegacyCloudWorker worker;
return worker;
}
#if WITH_CURL
int progress_callback_download(void* clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
@@ -233,28 +306,12 @@ void execute_cloud_download_thread(
execute_cloud_download_flow(app, request);
}
void launch_cloud_download_thread(
App& app,
const pp::app::CloudDownloadRequest& request)
{
std::thread([app = &app, request] {
execute_cloud_download_thread(*app, request);
}).detach();
}
void execute_cloud_publish_worker(App& app, bool save_before_upload)
{
BT_SetTerminate();
execute_cloud_publish_transfer_and_success_prompt(app, save_before_upload);
}
void launch_cloud_publish_thread(App& app, bool save_before_upload)
{
std::thread([app = &app, save_before_upload] {
execute_cloud_publish_worker(*app, save_before_upload);
}).detach();
}
void wire_cloud_publish_prompt_buttons(
const std::shared_ptr<NodeMessageBox>& dialog,
std::function<void()> upload_thread)
@@ -273,7 +330,9 @@ void setup_cloud_publish_prompt(App& app, bool save_before_upload)
const auto prompt_plan = pp::app::plan_cloud_publish_prompt();
auto dialog = app.message_box(prompt_plan.title, prompt_plan.message, prompt_plan.show_cancel);
wire_cloud_publish_prompt_buttons(dialog, [&app, save_before_upload] {
launch_cloud_publish_thread(app, save_before_upload);
queue_legacy_cloud_worker_task([app = &app, save_before_upload] {
execute_cloud_publish_worker(*app, save_before_upload);
});
});
}
@@ -379,7 +438,9 @@ public:
void start_download(const pp::app::CloudDownloadRequest& request) override
{
launch_cloud_download_thread(app_, request);
queue_legacy_cloud_worker_task([app = &app_, request] {
execute_cloud_download_thread(*app, request);
});
}
private:
@@ -395,6 +456,11 @@ void show_cloud_save_required_warning(App& app)
} // namespace
void queue_legacy_cloud_worker_task(std::function<void()> task)
{
cloud_worker().post(std::move(task));
}
pp::foundation::Status execute_legacy_cloud_upload_plan(
App& app,
const pp::app::CloudUploadPlan& plan)

View File

@@ -3,6 +3,8 @@
#include "app_core/document_cloud.h"
#include "foundation/result.h"
#include <functional>
class App;
class NodeDialogCloud;
@@ -22,4 +24,6 @@ namespace pp::panopainter {
pp::app::CloudDownloadSelectionAction action,
NodeDialogCloud& dialog);
void queue_legacy_cloud_worker_task(std::function<void()> task);
} // namespace pp::panopainter

View File

@@ -174,6 +174,25 @@ void close_legacy_overlay_handle_ignoring_status(
(void)close_legacy_overlay_node(anchor, overlay);
}
void close_legacy_overlay_handle_and_reset(
Node& anchor,
pp::ui::NodeHandle& overlay) noexcept
{
if (overlay.valid()) {
close_legacy_overlay_handle_ignoring_status(anchor, overlay);
overlay = {};
}
}
void close_legacy_overlay_handles_and_reset(
Node& anchor,
pp::ui::NodeHandle& popup_overlay,
pp::ui::NodeHandle& tick_overlay) noexcept
{
close_legacy_overlay_handle_and_reset(anchor, popup_overlay);
close_legacy_overlay_handle_and_reset(anchor, tick_overlay);
}
void close_legacy_overlay_handles_if_open(
Node& anchor,
const pp::foundation::Result<pp::ui::NodeHandle>& popup_overlay,

View File

@@ -111,6 +111,13 @@ void close_legacy_popup_overlay(Node& node) noexcept;
void close_legacy_overlay_handle_ignoring_status(
Node& anchor,
pp::ui::NodeHandle overlay) noexcept;
void close_legacy_overlay_handle_and_reset(
Node& anchor,
pp::ui::NodeHandle& overlay) noexcept;
void close_legacy_overlay_handles_and_reset(
Node& anchor,
pp::ui::NodeHandle& popup_overlay,
pp::ui::NodeHandle& tick_overlay) noexcept;
void close_legacy_overlay_handles_if_open(
Node& anchor,
const pp::foundation::Result<pp::ui::NodeHandle>& popup_overlay,

View File

@@ -932,8 +932,8 @@ int main(int argc, char** argv)
wglMakeCurrent(NULL, NULL);
running = 1;
App::I->render_thread_start();
App::I->ui_thread_start();
App::I->runtime().render_thread_start(*App::I);
App::I->runtime().ui_thread_start(*App::I);
#ifdef _DEBUG
glad_set_pre_callback(_pre_call_callback);
@@ -1035,8 +1035,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
running = 0;
if (hmd_renderer.joinable())
hmd_renderer.join();
App::I->ui_thread_stop();
App::I->render_thread_stop();
App::I->runtime().ui_thread_stop();
App::I->runtime().render_thread_stop();
App::I->terminate();
delete App::I;
PostQuitMessage(0);

View File

@@ -42,7 +42,7 @@
void Node::app_redraw()
{
App::I->redraw = true;
App::I->render_cv.notify_all();
App::I->runtime().notify_render_worker();
}
void Node::watch(std::function<bool(Node*)> observer)

View File

@@ -68,7 +68,7 @@ void NodeDialogCloud::init_controls()
btn_cancel = find<NodeButton>("btn-cancel");
pp::panopainter::bind_legacy_click_destroys_node(*btn_cancel, *this);
container = find<Node>("files-list");
std::thread(&NodeDialogCloud::load_thumbs_thread, this).detach();
load_thumbs_worker_ = std::jthread(&NodeDialogCloud::load_thumbs_thread, this);
}
void NodeDialogCloud::loaded()
@@ -79,6 +79,7 @@ void NodeDialogCloud::removed(Node* parent)
{
NodeBorder::removed(parent);
closed = true;
load_thumbs_worker_.request_stop();
}
NodeText* NodeDialogCloud::create_loading_status_text()
@@ -143,24 +144,30 @@ std::vector<NodeDialogCloudItem*> NodeDialogCloud::create_cloud_file_items(const
return nodes;
}
void NodeDialogCloud::load_thumbs_thread()
void NodeDialogCloud::load_thumbs_thread(std::stop_token stop)
{
#if WITH_CURL
BT_SetTerminate();
CURL *curl = curl_easy_init();
auto curl = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>(curl_easy_init(), curl_easy_cleanup);
std::string res;
if (curl)
{
if (stop.stop_requested() || closed)
return;
auto* text = create_loading_status_text();
auto* align = text->m_parent;
if (!load_cloud_file_list(curl, res, *text))
if (!load_cloud_file_list(curl.get(), res, *text))
{
return;
}
pp::panopainter::destroy_legacy_node(*align);
if (stop.stop_requested() || closed)
return;
LOG("CLOUD LIST: %s", res.c_str());
auto names = split(res, ',');
@@ -171,13 +178,12 @@ void NodeDialogCloud::load_thumbs_thread()
{
const auto& n = names[i];
auto* node = nodes[i];
if (closed)
if (stop.stop_requested() || closed)
break;
if (!load_cloud_thumb(curl, n, node, res))
if (!load_cloud_thumb(curl.get(), n, node, res))
break;
}
curl_easy_cleanup(curl);
}
#endif //CURL
}

View File

@@ -6,6 +6,8 @@
#include "node_text_input.h"
#include <vector>
#include <stop_token>
#include <thread>
class NodeDialogCloudItem : public NodeBorder
{
@@ -46,8 +48,11 @@ public:
void init_controls();
virtual void loaded() override;
virtual void removed(Node* parent) override;
void load_thumbs_thread();
void load_thumbs_thread(std::stop_token stop);
NodeText* create_loading_status_text();
bool load_cloud_file_list(CURL* curl, std::string& response, NodeText& status_text);
std::vector<NodeDialogCloudItem*> create_cloud_file_items(const std::vector<std::string>& names);
private:
std::jthread load_thumbs_worker_;
};

View File

@@ -722,14 +722,10 @@ void NodePanelStroke::execute_stroke_control_plan(const pp::app::BrushStrokeCont
void NodePanelStroke::close_popup_overlay_handles() noexcept
{
if (m_popup_overlay_handle.valid()) {
pp::panopainter::close_legacy_overlay_handle_ignoring_status(*this, m_popup_overlay_handle);
m_popup_overlay_handle = {};
}
if (m_tick_overlay_handle.valid()) {
pp::panopainter::close_legacy_overlay_handle_ignoring_status(*this, m_tick_overlay_handle);
m_tick_overlay_handle = {};
}
pp::panopainter::close_legacy_overlay_handles_and_reset(
*this,
m_popup_overlay_handle,
m_tick_overlay_handle);
}
kEventResult NodePanelStroke::handle_event(Event* e)

View File

@@ -4,6 +4,7 @@
#include "node_popup_menu.h"
#include "node_button_custom.h"
#include "app.h"
#include <memory>
Node* NodePopupMenu::clone_instantiate() const
{
@@ -33,19 +34,22 @@ kEventResult NodePopupMenu::handle_event(Event* e)
case kEventType::MouseDownL:
break;
case kEventType::MouseUpL:
if (m_mouse_inside)
{
for (int i = 0; i < m_children.size(); i++)
auto self = std::static_pointer_cast<NodePopupMenu>(shared_from_this());
if (m_mouse_inside)
{
if (m_children[i]->m_mouse_inside)
for (int i = 0; i < m_children.size(); i++)
{
if (on_select)
on_select(this, i);
break;
if (m_children[i]->m_mouse_inside)
{
if (on_select)
on_select(self.get(), i);
break;
}
}
}
close_popup();
}
close_popup();
break;
default:
return kEventResult::Available;
@@ -61,13 +65,19 @@ void NodePopupMenu::added(Node* parent)
m_mouse_ignore = false;
m_flood_events = true;
m_capture_children = false;
auto self = std::static_pointer_cast<NodePopupMenu>(shared_from_this());
for (int i = 0; i < m_children.size(); i++)
{
if (auto b = std::dynamic_pointer_cast<NodeButtonCustom>(m_children[i]))
{
b->on_click = [this, i](Node* target) {
if (on_select)
on_select(this, i);
std::weak_ptr<NodePopupMenu> weak_self = self;
b->on_click = [weak_self, i](Node* target) {
auto self = weak_self.lock();
if (!self) {
return;
}
if (self->on_select)
self->on_select(self.get(), i);
};
}
}

7
test.cpp Normal file
View File

@@ -0,0 +1,7 @@
#include <memory>
struct A{};
int main(){
std::shared_ptr<A> p;
A* a = nullptr;
p = a;
}