use bevy::asset::UnapprovedPathMode;
use bevy::{
app::AppExit,
input::{
ButtonState,
keyboard::{Key, KeyboardInput},
mouse::{MouseButtonInput, MouseWheel},
},
prelude::*,
reflect::{
ApplyError, DynamicTypePath, GetTypeRegistration, ReflectMut, ReflectOwned, ReflectRef,
TypeInfo,
},
render::extract_resource::ExtractResource,
window::{ExitCondition, WindowClosed, WindowEvent, WindowFocused, WindowResized},
winit::UpdateMode,
};
use crate::NannouPlugin;
use crate::context::App;
use crate::prelude::render::NannouCamera;
use crate::{
frame::Frame,
prelude::{
bevy_ecs::system::SystemState,
bevy_reflect::{DynamicTyped, ReflectCloneError},
render::{NannouShaderModelPlugin, ShaderModel},
},
render::{
RenderApp, RenderPlugin,
compute::{Compute, ComputeModel, ComputePlugin, ComputeShaderHandle, ComputeState},
},
window,
window::WindowUserFunctions,
};
use find_folder;
use std::{any::Any, hash::Hash, path::PathBuf, time::Duration};
pub type ModelFn<Model> = fn(&App<'_, '_>) -> Model;
pub type ComputeUpdateFn<Model, ComputeModel> =
fn(
&App<'_, '_>,
&Model,
<ComputeModel as Compute>::State,
Entity,
) -> (<ComputeModel as Compute>::State, ComputeModel);
pub type EventFn<Model, Event> = fn(&App<'_, '_>, &mut Model, &Event);
pub type UpdateFn<Model> = fn(&App<'_, '_>, &mut Model);
pub type ViewFn<Model> = fn(&App<'_, '_>, &Model, view: Entity);
pub type SketchViewFn = fn(&App<'_, '_>);
pub type ExitFn<Model> = fn(&App<'_, '_>, Model);
pub type RenderFn<Model> = fn(&RenderApp, &Model, Frame);
enum View<Model = ()> {
WithModel(ViewFn<Model>),
Sketch(SketchViewFn),
}
impl<M> Clone for View<M> {
fn clone(&self) -> Self {
match self {
View::WithModel(view) => View::WithModel(*view),
View::Sketch(view) => View::Sketch(*view),
}
}
}
pub struct Builder<M = (), E = WindowEvent> {
app: bevy::app::App,
model: ModelFn<M>,
config: Config,
event: Option<EventFn<M, E>>,
update: Option<UpdateFn<M>>,
render: Option<RenderFn<M>>,
default_view: Option<View<M>>,
exit: Option<ExitFn<M>>,
synchronous_pipeline_compilation: bool,
plugins_initialized: bool,
}
pub struct SketchBuilder<E = WindowEvent> {
builder: Builder<(), E>,
}
#[derive(Debug, Clone)]
enum DefaultWindowSize {
Logical([u32; 2]),
Fullscreen,
}
fn default_model(_: &App<'_, '_>) {}
#[derive(Resource, Deref, DerefMut)]
struct ModelFnRes<M>(ModelFn<M>);
#[derive(Resource, Deref, DerefMut)]
struct EventFnRes<M, E>(Option<EventFn<M, E>>);
#[derive(Resource, Deref, DerefMut)]
struct UpdateFnRes<M>(Option<UpdateFn<M>>);
#[derive(Resource, Deref, DerefMut)]
struct ComputeUpdateFnRes<M, CM: Compute>(ComputeUpdateFn<M, CM>);
#[derive(Resource, Deref, DerefMut)]
pub(crate) struct RenderFnRes<M>(Option<RenderFn<M>>);
impl<M> ExtractResource for RenderFnRes<M>
where
M: Clone + Send + Sync + 'static,
{
type Source = Self;
fn extract_resource(source: &Self::Source) -> Self {
RenderFnRes(source.0.clone())
}
}
#[derive(Resource, Deref, DerefMut)]
struct ViewFnRes<M>(Option<View<M>>);
#[derive(Resource, Deref, DerefMut)]
struct ExitFnRes<M>(Option<ExitFn<M>>);
#[derive(Resource, Debug, Clone)]
struct Config {
default_window_size: Option<DefaultWindowSize>,
}
#[derive(Resource, Deref, DerefMut)]
pub struct ModelHolder<M>(pub M);
impl<M> ExtractResource for ModelHolder<M>
where
M: Clone + Send + Sync + 'static,
{
type Source = Self;
fn extract_resource(source: &Self::Source) -> Self {
ModelHolder(source.0.clone())
}
}
#[derive(Resource)]
struct CreateDefaultWindow;
#[derive(Resource, Default)]
pub enum RunMode {
#[default]
UntilExit,
Ticks(u64),
Duration(Duration),
}
impl RunMode {
pub fn once() -> Self {
RunMode::Ticks(1)
}
}
#[cfg(feature = "egui")]
#[allow(deprecated)]
fn egui_plugin() -> bevy_egui::EguiPlugin {
bevy_egui::EguiPlugin {
enable_multipass_for_primary_context: false,
..bevy_egui::EguiPlugin::default()
}
}
impl<M> Builder<M>
where
M: 'static,
{
pub fn new(model: ModelFn<M>) -> Self {
Builder {
app: bevy::app::App::new(),
model,
config: Config::default(),
event: None,
update: None,
render: None,
default_view: None,
exit: None,
synchronous_pipeline_compilation: true,
plugins_initialized: false,
}
}
}
impl<M, E> Builder<M, E>
where
M: 'static + Send + Sync,
E: Message,
{
fn ensure_initialized(&mut self) {
if self.plugins_initialized {
return;
}
self.plugins_initialized = true;
self.app
.add_plugins((
DefaultPlugins
.set(AssetPlugin {
unapproved_path_mode: UnapprovedPathMode::Allow,
..default()
})
.set(WindowPlugin {
#[cfg(not(target_arch = "wasm32"))]
primary_window: None,
#[cfg(target_arch = "wasm32")]
primary_window: Some(Window {
title: "Nannou".to_string(),
resolution: (1024.0, 768.0).into(),
present_mode: crate::window::DEFAULT_PRESENT_MODE,
..default()
}),
exit_condition: ExitCondition::OnAllClosed,
..default()
})
.set(bevy::render::RenderPlugin {
synchronous_pipeline_compilation: self.synchronous_pipeline_compilation,
..default()
}),
#[cfg(feature = "egui")]
egui_plugin(),
NannouPlugin,
))
.init_resource::<RunMode>();
}
pub fn synchronous_pipeline_compilation(mut self, synchronous: bool) -> Self {
assert!(
!self.plugins_initialized,
"`synchronous_pipeline_compilation` must be set before other builder \
methods that configure the app"
);
self.synchronous_pipeline_compilation = synchronous;
self
}
pub fn view(mut self, view: ViewFn<M>) -> Self {
self.default_view = Some(View::WithModel(view));
self
}
pub fn update(mut self, update: UpdateFn<M>) -> Self {
self.update = Some(update);
self
}
pub fn render(mut self, render: RenderFn<M>) -> Self
where
M: Send + Sync + Clone + 'static,
{
self.render = Some(render);
self.ensure_initialized();
self.app.add_plugins(RenderPlugin::<M>::default());
self
}
pub fn simple_window(mut self, view: ViewFn<M>) -> Self {
self.default_view = Some(View::WithModel(view));
self.ensure_initialized();
self.app.insert_resource(CreateDefaultWindow);
self
}
pub fn exit(mut self, exit: ExitFn<M>) -> Self {
self.exit = Some(exit);
self
}
pub fn set_run_mode(mut self, run_mode: RunMode) -> Self {
self.ensure_initialized();
self.app.insert_resource(run_mode);
self
}
pub fn shader_model<SM>(mut self) -> Self
where
SM: ShaderModel,
SM::Data: PartialEq + Eq + Hash + Clone,
{
self.ensure_initialized();
self.app
.add_plugins((NannouShaderModelPlugin::<SM>::default(),));
self
}
pub fn compute<CM: Compute>(mut self, compute_fn: ComputeUpdateFn<M, CM>) -> Self {
self.ensure_initialized();
let render_app = self.app.sub_app_mut(bevy::render::RenderApp);
render_app.insert_resource(ComputeShaderHandle(CM::shader()));
self.app
.add_systems(
First,
|mut commands: Commands, views_q: Query<Entity, Added<NannouCamera>>| {
for view in views_q.iter() {
info!("Adding compute state to view {:?}", view);
commands.entity(view).insert(ComputeState {
current: CM::State::default(),
next: None,
next_ready: false,
});
}
},
)
.insert_resource(ComputeUpdateFnRes(compute_fn))
.add_systems(Update, compute::<M, CM>.after(update::<M>))
.add_systems(Last, |_query: Query<&ComputeModel<CM>>| {})
.add_plugins(ComputePlugin::<CM>::default());
self
}
pub fn add_plugin<P>(mut self, plugin: P) -> Self
where
P: Plugin,
{
self.ensure_initialized();
self.app.add_plugins(plugin);
self
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.config.default_window_size = Some(DefaultWindowSize::Logical([width, height]));
self
}
pub fn fullscreen(mut self) -> Self {
self.config.default_window_size = Some(DefaultWindowSize::Fullscreen);
self
}
pub fn run(mut self) {
self.ensure_initialized();
self.app
.insert_resource(self.config.clone())
.insert_resource(ModelFnRes(self.model))
.insert_resource(EventFnRes(self.event))
.insert_resource(UpdateFnRes(self.update))
.insert_resource(RenderFnRes(self.render))
.insert_resource(ViewFnRes(self.default_view))
.insert_resource(ExitFnRes(self.exit))
.add_systems(Startup, startup::<M>)
.add_systems(
Update,
(
update::<M>,
events::<M, E>,
key_events::<M>,
received_char_events::<M>,
cursor_moved_events::<M>,
mouse_button_events::<M>,
cursor_entered_events::<M>,
cursor_left_events::<M>,
mouse_wheel_events::<M>,
window_moved_events::<M>,
window_resized_events::<M>,
touch_events::<M>,
file_drop_events::<M>,
window_focus_events::<M>,
window_closed_events::<M>,
),
)
.add_systems(Last, last::<M>)
.run();
}
}
impl SketchBuilder {
pub fn size(mut self, width: u32, height: u32) -> Self {
self.builder = self.builder.size(width, height);
self
}
pub fn run(self) {
self.builder.run()
}
}
impl Builder<()> {
pub fn sketch(view: SketchViewFn) -> SketchBuilder {
let mut builder = Builder::new(default_model);
builder.default_view = Some(View::Sketch(view));
builder.ensure_initialized();
builder.app.insert_resource(CreateDefaultWindow);
SketchBuilder { builder }
}
}
impl Default for Config {
fn default() -> Self {
Config {
default_window_size: None,
}
}
}
impl<M> GetTypeRegistration for ModelHolder<M>
where
M: GetTypeRegistration,
{
fn get_type_registration() -> bevy::reflect::TypeRegistration {
M::get_type_registration()
}
}
impl<M> DynamicTypePath for ModelHolder<M>
where
M: DynamicTypePath,
{
fn reflect_type_path(&self) -> &str {
self.0.reflect_type_path()
}
fn reflect_short_type_path(&self) -> &str {
self.0.reflect_short_type_path()
}
fn reflect_type_ident(&self) -> Option<&str> {
self.0.reflect_type_ident()
}
fn reflect_crate_name(&self) -> Option<&str> {
self.0.reflect_crate_name()
}
fn reflect_module_path(&self) -> Option<&str> {
self.0.reflect_module_path()
}
}
impl<M> PartialReflect for ModelHolder<M>
where
M: PartialReflect + DynamicTypePath + Any + GetTypeRegistration + 'static,
{
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
self.0.get_represented_type_info()
}
fn into_partial_reflect(self: Box<Self>) -> Box<dyn PartialReflect> {
Box::new(ModelHolder(self.0))
}
fn as_partial_reflect(&self) -> &dyn PartialReflect {
self.0.as_partial_reflect()
}
fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect {
self.0.as_partial_reflect_mut()
}
fn try_into_reflect(self: Box<Self>) -> Result<Box<dyn Reflect>, Box<dyn PartialReflect>> {
Box::new(self.0).try_into_reflect()
}
fn try_as_reflect(&self) -> Option<&dyn Reflect> {
self.0.try_as_reflect()
}
fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> {
self.0.try_as_reflect_mut()
}
fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> {
self.0.try_apply(value)
}
fn reflect_ref(&self) -> ReflectRef<'_> {
self.0.reflect_ref()
}
fn reflect_mut(&mut self) -> ReflectMut<'_> {
self.0.reflect_mut()
}
fn reflect_owned(self: Box<Self>) -> ReflectOwned {
Box::new(self.0).reflect_owned()
}
fn reflect_clone(&self) -> std::result::Result<Box<dyn Reflect>, ReflectCloneError> {
self.0.reflect_clone()
}
}
impl<M> DynamicTyped for ModelHolder<M>
where
M: 'static + Any + DynamicTypePath + GetTypeRegistration + Reflect,
{
fn reflect_type_info(&self) -> &'static TypeInfo {
self.0.reflect_type_info()
}
}
impl<M> Reflect for ModelHolder<M>
where
M: Reflect + DynamicTypePath + Any + GetTypeRegistration + 'static,
{
fn into_any(self: Box<Self>) -> Box<dyn Any> {
Box::new(self.0).into_any()
}
fn as_any(&self) -> &dyn Any {
self.0.as_any()
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self.0.as_any_mut()
}
fn into_reflect(self: Box<Self>) -> Box<dyn Reflect> {
Box::new(self.0).into_reflect()
}
fn as_reflect(&self) -> &dyn Reflect {
self.0.as_reflect()
}
fn as_reflect_mut(&mut self) -> &mut dyn Reflect {
self.0.as_reflect_mut()
}
fn set(&mut self, value: Box<dyn Reflect>) -> Result<(), Box<dyn Reflect>> {
self.0.set(value)
}
}
fn startup<M>(
app: App,
config: Res<Config>,
model_fn: Res<ModelFnRes<M>>,
create_default_window: Option<Res<CreateDefaultWindow>>,
) where
M: 'static + Send + Sync,
{
if create_default_window.is_some() {
let mut window = app.new_window::<M>();
match &config.default_window_size {
None => {}
Some(DefaultWindowSize::Logical([w, h])) => {
window = window.size(*w, *h);
}
Some(DefaultWindowSize::Fullscreen) => {
window = window.fullscreen();
}
}
let _ = window.primary().build();
}
let model_fn = model_fn.0;
let model = model_fn(&app);
app.command_scope(move |mut commands| {
commands.insert_resource(ModelHolder(model));
});
}
#[allow(clippy::type_complexity)]
fn update<M>(
app: App,
update_fn: Res<UpdateFnRes<M>>,
view_fn: Res<ViewFnRes<M>>,
mut model: ResMut<ModelHolder<M>>,
run_mode: Res<RunMode>,
time: Res<Time>,
mut ticks: Local<u64>,
windows: Query<(Entity, &WindowUserFunctions<M>)>,
) where
M: 'static + Send + Sync,
{
match *run_mode {
RunMode::UntilExit => {
}
RunMode::Ticks(run_ticks) => {
if *ticks >= run_ticks {
app.quit();
return;
}
}
RunMode::Duration(duration) => {
if time.elapsed() >= duration {
app.quit();
return;
}
}
};
app.set_current_view(None);
if let Some(update_fn) = update_fn.0 {
update_fn(&app, &mut model);
}
for (entity, user_fns) in windows.iter() {
app.set_current_view(Some(entity));
if let Some(view) = &user_fns.view {
match view {
window::View::WithModel(view_fn) => {
view_fn(&app, &model);
}
window::View::Sketch(view_fn) => {
view_fn(&app);
}
}
} else if let Some(view) = view_fn.0.as_ref() {
match view {
View::WithModel(view_fn) => {
view_fn(&app, &model, entity);
}
View::Sketch(view_fn) => {
view_fn(&app);
}
}
}
}
*ticks += 1;
}
#[allow(clippy::type_complexity)]
fn compute<M, CM>(
app: App,
model: Res<ModelHolder<M>>,
compute: Res<ComputeUpdateFnRes<M, CM>>,
views_q: Query<(Entity, &ComputeState<CM::State>)>,
) where
M: 'static + Send + Sync,
CM: Compute,
{
let compute = compute.0;
for (view, state) in views_q.iter() {
let current = match (state.next_ready, &state.next) {
(true, Some(next)) => next.clone(),
_ => state.current.clone(),
};
let (new_state, compute_model) = compute(&app, &model, current.clone(), view);
let next = (new_state != current).then_some(new_state);
app.command_scope(move |mut commands| {
commands.entity(view).insert((
ComputeState {
current,
next,
next_ready: false,
},
ComputeModel(compute_model),
));
});
}
}
fn events<M, E>(
app: App,
mut events: MessageReader<E>,
event_fn: Res<EventFnRes<M, E>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: Send + Sync + 'static,
E: Message,
{
for evt in events.read() {
if let Some(f) = event_fn.0.as_ref() {
f(&app, &mut model, evt);
}
}
}
macro_rules! window_event_driver {
($name:ident, $msg:ty, $field:ident) => {
fn $name<M>(
app: App,
mut events: MessageReader<$msg>,
user_fns: Query<&WindowUserFunctions<M>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: 'static + Send + Sync,
{
for evt in events.read() {
if let Ok(user_fns) = user_fns.get(evt.window) {
if let Some(f) = user_fns.$field {
app.set_current_view(Some(evt.window));
f(&app, &mut model);
}
}
}
}
};
($name:ident, $msg:ty, $field:ident, |$app:ident, $evt:ident| $arg:expr) => {
fn $name<M>(
app: App,
mut events: MessageReader<$msg>,
user_fns: Query<&WindowUserFunctions<M>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: 'static + Send + Sync,
{
for evt in events.read() {
if let Ok(user_fns) = user_fns.get(evt.window) {
if let Some(f) = user_fns.$field {
app.set_current_view(Some(evt.window));
f(&app, &mut model, {
let $app = &app;
let $evt = evt;
$arg
});
}
}
}
}
};
($name:ident, $msg:ty, $field:ident, |$evt:ident| $arg:expr) => {
window_event_driver!($name, $msg, $field, |_app, $evt| $arg);
};
}
macro_rules! button_event_driver {
($name:ident, $msg:ty, $pressed:ident, $released:ident, |$evt:ident| $arg:expr) => {
fn $name<M>(
app: App,
mut events: MessageReader<$msg>,
user_fns: Query<&WindowUserFunctions<M>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: 'static + Send + Sync,
{
for evt in events.read() {
if let Ok(user_fns) = user_fns.get(evt.window) {
let f = match evt.state {
ButtonState::Pressed => user_fns.$pressed,
ButtonState::Released => user_fns.$released,
};
if let Some(f) = f {
app.set_current_view(Some(evt.window));
f(&app, &mut model, {
let $evt = evt;
$arg
});
}
}
}
}
};
}
button_event_driver!(
key_events,
KeyboardInput,
key_pressed,
key_released,
|evt| evt.key_code
);
#[allow(clippy::type_complexity)]
fn received_char_events<M>(
app: App,
mut received_char_events: MessageReader<KeyboardInput>,
user_fns: Query<&WindowUserFunctions<M>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: 'static + Send + Sync,
{
for evt in received_char_events.read() {
if let Ok(user_fns) = user_fns.get(evt.window) {
if let Some(f) = user_fns.received_character {
app.set_current_view(Some(evt.window));
if let Key::Character(char) = &evt.logical_key {
for char in char.chars() {
f(&app, &mut model, char);
}
}
}
}
}
}
window_event_driver!(cursor_moved_events, CursorMoved, mouse_moved, |app, evt| {
app.screen_to_points(evt.window, evt.position)
});
button_event_driver!(
mouse_button_events,
MouseButtonInput,
mouse_pressed,
mouse_released,
|evt| evt.button
);
window_event_driver!(cursor_entered_events, CursorEntered, mouse_entered);
window_event_driver!(cursor_left_events, CursorLeft, mouse_exited);
window_event_driver!(mouse_wheel_events, MouseWheel, mouse_wheel, |evt| *evt);
window_event_driver!(window_moved_events, WindowMoved, moved, |evt| evt.position);
window_event_driver!(window_resized_events, WindowResized, resized, |evt| {
Vec2::new(evt.width, evt.height)
});
window_event_driver!(touch_events, TouchInput, touch, |app, evt| {
let mut evt = *evt;
evt.position = app.screen_to_points(evt.window, evt.position);
evt
});
#[allow(clippy::type_complexity)]
fn file_drop_events<M>(
app: App,
mut file_drop_events: MessageReader<FileDragAndDrop>,
user_fns: Query<&WindowUserFunctions<M>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: 'static + Send + Sync,
{
for evt in file_drop_events.read() {
match evt {
FileDragAndDrop::DroppedFile { window, path_buf } => {
if let Ok(user_fns) = user_fns.get(*window) {
if let Some(f) = user_fns.dropped_file {
app.set_current_view(Some(*window));
f(&app, &mut model, path_buf.clone());
}
}
}
FileDragAndDrop::HoveredFile { window, path_buf } => {
if let Ok(user_fns) = user_fns.get(*window) {
if let Some(f) = user_fns.hovered_file {
app.set_current_view(Some(*window));
f(&app, &mut model, path_buf.clone());
}
}
}
FileDragAndDrop::HoveredFileCanceled { window } => {
if let Ok(user_fns) = user_fns.get(*window) {
if let Some(f) = user_fns.hovered_file_cancelled {
app.set_current_view(Some(*window));
f(&app, &mut model);
}
}
}
}
}
}
#[allow(clippy::type_complexity)]
fn window_focus_events<M>(
app: App,
mut window_focus_events: MessageReader<WindowFocused>,
user_fns: Query<&WindowUserFunctions<M>>,
mut model: ResMut<ModelHolder<M>>,
) where
M: 'static + Send + Sync,
{
for evt in window_focus_events.read() {
if let Ok(user_fns) = user_fns.get(evt.window) {
if evt.focused {
if let Some(f) = user_fns.focused {
app.set_current_view(Some(evt.window));
f(&app, &mut model);
}
} else if let Some(f) = user_fns.unfocused {
app.set_current_view(Some(evt.window));
f(&app, &mut model);
}
}
}
}
window_event_driver!(window_closed_events, WindowClosed, closed);
fn last<M>(world: &mut World, exit_state: &mut SystemState<MessageReader<AppExit>>)
where
M: 'static + Send + Sync,
{
let should_exit = {
let exit_events = exit_state
.get(world)
.expect("failed to fetch system params");
!exit_events.is_empty()
};
if !should_exit {
return;
}
let exit_fn = world.resource::<ExitFnRes<M>>().0;
let Some(model) = world.remove_resource::<ModelHolder<M>>() else {
return;
};
if let Some(exit_fn) = exit_fn {
let mut app_state = SystemState::<App>::new(world);
{
let app = app_state.get(world).expect("failed to fetch system params");
exit_fn(&app, model.0);
}
app_state.apply(world);
}
}
const WAIT_INDEFINITELY: Duration = Duration::from_secs(u32::MAX as u64);
pub trait UpdateModeExt {
fn wait() -> UpdateMode;
fn freeze() -> UpdateMode;
fn rate(hz: f64) -> UpdateMode;
}
impl UpdateModeExt for UpdateMode {
fn wait() -> UpdateMode {
UpdateMode::Reactive {
wait: WAIT_INDEFINITELY,
react_to_device_events: true,
react_to_user_events: true,
react_to_window_events: true,
}
}
fn freeze() -> UpdateMode {
UpdateMode::Reactive {
wait: WAIT_INDEFINITELY,
react_to_device_events: false,
react_to_user_events: false,
react_to_window_events: true,
}
}
fn rate(hz: f64) -> UpdateMode {
if hz <= 0.0 {
return UpdateMode::freeze();
}
UpdateMode::Reactive {
wait: Duration::from_secs_f64(1.0 / hz),
react_to_device_events: false,
react_to_user_events: false,
react_to_window_events: false,
}
}
}
pub fn find_project_path() -> Result<PathBuf, find_folder::Error> {
let exe_path = std::env::current_exe()?;
let mut path = exe_path.parent().expect("exe has no parent directory");
while let Some(parent) = path.parent() {
path = parent;
if path.join("Cargo").with_extension("toml").exists() {
return Ok(path.to_path_buf());
}
}
Err(find_folder::Error::NotFound)
}