epic: refactor SDL2_Renderer usage to IRenderer and queued texture rendering
- Updated the testing guide to clarify the testing framework's organization and execution methods, improving user understanding. - Refactored CMakeLists to include new platform-specific files, ensuring proper integration of the rendering backend. - Modified main application files to utilize the new IRenderer interface, enhancing flexibility in rendering operations. - Implemented deferred texture management in various components, allowing for more efficient graphics handling and improved performance. - Introduced new methods for texture creation and updates, streamlining the rendering process across the application. - Enhanced logging and error handling in the rendering pipeline to facilitate better debugging and diagnostics.
This commit is contained in:
64
src/app/platform/app_delegate.h
Normal file
64
src/app/platform/app_delegate.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#ifndef YAZE_APP_PLATFORM_APP_DELEGATE_H
|
||||
#define YAZE_APP_PLATFORM_APP_DELEGATE_H
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). */
|
||||
#import <CoreText/CoreText.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
|
||||
/* iOS in Xcode simulator */
|
||||
#import <PencilKit/PencilKit.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate,
|
||||
UIDocumentPickerDelegate,
|
||||
UITabBarControllerDelegate,
|
||||
PKCanvasViewDelegate>
|
||||
@property(strong, nonatomic) UIWindow *window;
|
||||
|
||||
@property UIDocumentPickerViewController *documentPicker;
|
||||
@property(nonatomic, copy) void (^completionHandler)(NSString *selectedFile);
|
||||
- (void)PresentDocumentPickerWithCompletionHandler:
|
||||
(void (^)(NSString *selectedFile))completionHandler;
|
||||
|
||||
// TODO: Setup a tab bar controller for multiple yaze instances
|
||||
@property(nonatomic) UITabBarController *tabBarController;
|
||||
|
||||
// TODO: Setup a font picker for the text editor and display settings
|
||||
@property(nonatomic) UIFontPickerViewController *fontPicker;
|
||||
|
||||
// TODO: Setup the pencil kit for drawing
|
||||
@property PKToolPicker *toolPicker;
|
||||
@property PKCanvasView *canvasView;
|
||||
|
||||
// TODO: Setup the file manager for file operations
|
||||
@property NSFileManager *fileManager;
|
||||
|
||||
@end
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize the Cocoa application.
|
||||
*/
|
||||
void yaze_initialize_cocoa();
|
||||
|
||||
/**
|
||||
* @brief Run the Cocoa application delegate.
|
||||
*/
|
||||
int yaze_run_cocoa_app_delegate(const char *filename);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif // TARGET_OS_MAC
|
||||
|
||||
#endif // defined(__APPLE__) && defined(__MACH__)
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_APP_DELEGATE_H
|
||||
264
src/app/platform/app_delegate.mm
Normal file
264
src/app/platform/app_delegate.mm
Normal file
@@ -0,0 +1,264 @@
|
||||
// AppDelegate.mm
|
||||
#import "app/platform/app_delegate.h"
|
||||
#import "app/core/controller.h"
|
||||
#import "util/file_util.h"
|
||||
#import "app/editor/editor.h"
|
||||
#import "app/rom.h"
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). */
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
|
||||
/* iOS in Xcode simulator */
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
/* macOS */
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
- (void)setupMenus;
|
||||
// - (void)changeApplicationIcon;
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
// - (void)changeApplicationIcon {
|
||||
// NSImage *newIcon = [NSImage imageNamed:@"newIcon"];
|
||||
// [NSApp setApplicationIconImage:newIcon];
|
||||
// }
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
[self setupMenus];
|
||||
}
|
||||
|
||||
- (void)setupMenus {
|
||||
NSMenu *mainMenu = [NSApp mainMenu];
|
||||
|
||||
NSMenuItem *fileMenuItem = [mainMenu itemWithTitle:@"File"];
|
||||
if (!fileMenuItem) {
|
||||
NSMenu *fileMenu = [[NSMenu alloc] initWithTitle:@"File"];
|
||||
fileMenuItem = [[NSMenuItem alloc] initWithTitle:@"File" action:nil keyEquivalent:@""];
|
||||
[fileMenuItem setSubmenu:fileMenu];
|
||||
|
||||
NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open"
|
||||
action:@selector(openFileAction:)
|
||||
keyEquivalent:@"o"];
|
||||
[fileMenu addItem:openItem];
|
||||
|
||||
// Open Recent
|
||||
NSMenu *openRecentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"];
|
||||
NSMenuItem *openRecentMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open Recent"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[openRecentMenuItem setSubmenu:openRecentMenu];
|
||||
[fileMenu addItem:openRecentMenuItem];
|
||||
|
||||
// Add a separator
|
||||
[fileMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
// Save
|
||||
NSMenuItem *saveItem = [[NSMenuItem alloc] initWithTitle:@"Save" action:nil keyEquivalent:@"s"];
|
||||
[fileMenu addItem:saveItem];
|
||||
|
||||
// Separator
|
||||
[fileMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
// Options submenu
|
||||
NSMenu *optionsMenu = [[NSMenu alloc] initWithTitle:@"Options"];
|
||||
NSMenuItem *optionsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Options"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[optionsMenuItem setSubmenu:optionsMenu];
|
||||
|
||||
// Flag checkmark field
|
||||
NSMenuItem *flagItem = [[NSMenuItem alloc] initWithTitle:@"Flag"
|
||||
action:@selector(toggleFlagAction:)
|
||||
keyEquivalent:@""];
|
||||
[flagItem setTarget:self];
|
||||
[flagItem setState:NSControlStateValueOff];
|
||||
[optionsMenu addItem:flagItem];
|
||||
[fileMenu addItem:optionsMenuItem];
|
||||
|
||||
[mainMenu insertItem:fileMenuItem atIndex:1];
|
||||
}
|
||||
|
||||
NSMenuItem *editMenuItem = [mainMenu itemWithTitle:@"Edit"];
|
||||
if (!editMenuItem) {
|
||||
NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
|
||||
editMenuItem = [[NSMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:@""];
|
||||
[editMenuItem setSubmenu:editMenu];
|
||||
|
||||
NSMenuItem *undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:nil keyEquivalent:@"z"];
|
||||
|
||||
[editMenu addItem:undoItem];
|
||||
|
||||
NSMenuItem *redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:nil keyEquivalent:@"Z"];
|
||||
|
||||
[editMenu addItem:redoItem];
|
||||
|
||||
// Add a separator
|
||||
[editMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
NSMenuItem *cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut"
|
||||
action:@selector(cutAction:)
|
||||
keyEquivalent:@"x"];
|
||||
[editMenu addItem:cutItem];
|
||||
|
||||
NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:nil keyEquivalent:@"c"];
|
||||
[editMenu addItem:copyItem];
|
||||
|
||||
NSMenuItem *pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste"
|
||||
action:nil
|
||||
keyEquivalent:@"v"];
|
||||
|
||||
[editMenu addItem:pasteItem];
|
||||
|
||||
// Add a separator
|
||||
[editMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
NSMenuItem *selectAllItem = [[NSMenuItem alloc] initWithTitle:@"Select All"
|
||||
action:nil
|
||||
keyEquivalent:@"a"];
|
||||
|
||||
[editMenu addItem:selectAllItem];
|
||||
|
||||
[mainMenu insertItem:editMenuItem atIndex:2];
|
||||
}
|
||||
|
||||
NSMenuItem *viewMenuItem = [mainMenu itemWithTitle:@"View"];
|
||||
if (!viewMenuItem) {
|
||||
NSMenu *viewMenu = [[NSMenu alloc] initWithTitle:@"View"];
|
||||
viewMenuItem = [[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""];
|
||||
[viewMenuItem setSubmenu:viewMenu];
|
||||
|
||||
// Emulator view button
|
||||
NSMenuItem *emulatorViewItem = [[NSMenuItem alloc] initWithTitle:@"Emulator View"
|
||||
action:nil
|
||||
keyEquivalent:@"1"];
|
||||
|
||||
[viewMenu addItem:emulatorViewItem];
|
||||
|
||||
// Hex Editor View
|
||||
NSMenuItem *hexEditorViewItem = [[NSMenuItem alloc] initWithTitle:@"Hex Editor View"
|
||||
action:nil
|
||||
keyEquivalent:@"2"];
|
||||
|
||||
[viewMenu addItem:hexEditorViewItem];
|
||||
|
||||
// Disassembly view button
|
||||
NSMenuItem *disassemblyViewItem = [[NSMenuItem alloc] initWithTitle:@"Disassembly View"
|
||||
action:nil
|
||||
keyEquivalent:@"3"];
|
||||
|
||||
[viewMenu addItem:disassemblyViewItem];
|
||||
|
||||
// Memory view button
|
||||
NSMenuItem *memoryViewItem = [[NSMenuItem alloc] initWithTitle:@"Memory View"
|
||||
action:nil
|
||||
keyEquivalent:@"4"];
|
||||
|
||||
[viewMenu addItem:memoryViewItem];
|
||||
|
||||
// Add a separator
|
||||
[viewMenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
// Toggle fullscreen button
|
||||
NSMenuItem *toggleFullscreenItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Fullscreen"
|
||||
action:nil
|
||||
keyEquivalent:@"f"];
|
||||
|
||||
[viewMenu addItem:toggleFullscreenItem];
|
||||
|
||||
[mainMenu insertItem:viewMenuItem atIndex:3];
|
||||
}
|
||||
|
||||
NSMenuItem *helpMenuItem = [mainMenu itemWithTitle:@"Help"];
|
||||
if (!helpMenuItem) {
|
||||
NSMenu *helpMenu = [[NSMenu alloc] initWithTitle:@"Help"];
|
||||
helpMenuItem = [[NSMenuItem alloc] initWithTitle:@"Help" action:nil keyEquivalent:@""];
|
||||
[helpMenuItem setSubmenu:helpMenu];
|
||||
|
||||
// URL to online documentation
|
||||
NSMenuItem *documentationItem = [[NSMenuItem alloc] initWithTitle:@"Documentation"
|
||||
action:nil
|
||||
keyEquivalent:@"?"];
|
||||
[helpMenu addItem:documentationItem];
|
||||
|
||||
[mainMenu insertItem:helpMenuItem atIndex:4];
|
||||
}
|
||||
}
|
||||
|
||||
// Action method for the New menu item
|
||||
- (void)newFileAction:(id)sender {
|
||||
NSLog(@"New File action triggered");
|
||||
}
|
||||
|
||||
- (void)toggleFlagAction:(id)sender {
|
||||
NSMenuItem *flagItem = (NSMenuItem *)sender;
|
||||
if ([flagItem state] == NSControlStateValueOff) {
|
||||
[flagItem setState:NSControlStateValueOn];
|
||||
} else {
|
||||
[flagItem setState:NSControlStateValueOff];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openFileAction:(id)sender {
|
||||
// TODO: Re-implmenent this without the SharedRom singleton
|
||||
// if (!yaze::SharedRom::shared_rom_
|
||||
// ->LoadFromFile(yaze::util::FileDialogWrapper::ShowOpenFileDialog())
|
||||
// .ok()) {
|
||||
// NSAlert *alert = [[NSAlert alloc] init];
|
||||
// [alert setMessageText:@"Error"];
|
||||
// [alert setInformativeText:@"Failed to load file."];
|
||||
// [alert addButtonWithTitle:@"OK"];
|
||||
// [alert runModal];
|
||||
// }
|
||||
}
|
||||
|
||||
- (void)cutAction:(id)sender {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
- (void)openRecentFileAction:(id)sender {
|
||||
NSLog(@"Open Recent File action triggered");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
extern "C" void yaze_initialize_cococa() {
|
||||
@autoreleasepool {
|
||||
AppDelegate *delegate = [[AppDelegate alloc] init];
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp setDelegate:delegate];
|
||||
[NSApp finishLaunching];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int yaze_run_cocoa_app_delegate(const char *filename) {
|
||||
yaze_initialize_cococa();
|
||||
auto controller = std::make_unique<yaze::core::Controller>();
|
||||
EXIT_IF_ERROR(controller->OnEntry(filename));
|
||||
while (controller->IsActive()) {
|
||||
@autoreleasepool {
|
||||
controller->OnInput();
|
||||
if (auto status = controller->OnLoad(); !status.ok()) {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:@"Error"];
|
||||
[alert setInformativeText:[NSString stringWithUTF8String:status.message().data()]];
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
[alert runModal];
|
||||
break;
|
||||
}
|
||||
controller->DoRender();
|
||||
}
|
||||
}
|
||||
controller->OnExit();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
94
src/app/platform/asset_loader.cc
Normal file
94
src/app/platform/asset_loader.cc
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "app/platform/asset_loader.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "util/file_util.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
std::vector<std::filesystem::path> AssetLoader::GetSearchPaths(const std::string& relative_path) {
|
||||
std::vector<std::filesystem::path> search_paths;
|
||||
|
||||
#ifdef __APPLE__
|
||||
// macOS bundle resource paths
|
||||
std::string bundle_root = yaze::util::GetBundleResourcePath();
|
||||
|
||||
// Try Contents/Resources first (standard bundle location)
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / "Contents" / "Resources" / relative_path);
|
||||
|
||||
// Try without Contents (if app is at root)
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / "Resources" / relative_path);
|
||||
|
||||
// Development paths (when running from build dir)
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / ".." / ".." / ".." / "assets" / relative_path);
|
||||
search_paths.push_back(std::filesystem::path(bundle_root) / ".." / ".." / ".." / ".." / "assets" / relative_path);
|
||||
#endif
|
||||
|
||||
// Standard relative paths (works for all platforms)
|
||||
search_paths.push_back(std::filesystem::path("assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../../assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../../../assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../../../../assets") / relative_path);
|
||||
|
||||
// Build directory paths
|
||||
search_paths.push_back(std::filesystem::path("build/assets") / relative_path);
|
||||
search_paths.push_back(std::filesystem::path("../build/assets") / relative_path);
|
||||
|
||||
return search_paths;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::filesystem::path> AssetLoader::FindAssetFile(const std::string& relative_path) {
|
||||
auto search_paths = GetSearchPaths(relative_path);
|
||||
|
||||
for (const auto& path : search_paths) {
|
||||
if (std::filesystem::exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Print searched paths
|
||||
std::string searched_paths;
|
||||
for (const auto& path : search_paths) {
|
||||
searched_paths += "\n - " + path.string();
|
||||
}
|
||||
|
||||
return absl::NotFoundError(
|
||||
absl::StrFormat("Asset file not found: %s\nSearched paths:%s",
|
||||
relative_path, searched_paths));
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> AssetLoader::LoadTextFile(const std::string& relative_path) {
|
||||
auto path_result = FindAssetFile(relative_path);
|
||||
if (!path_result.ok()) {
|
||||
return path_result.status();
|
||||
}
|
||||
|
||||
const auto& path = *path_result;
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to open file: %s", path.string()));
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
|
||||
if (content.empty()) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("File is empty: %s", path.string()));
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
bool AssetLoader::AssetExists(const std::string& relative_path) {
|
||||
return FindAssetFile(relative_path).ok();
|
||||
}
|
||||
|
||||
|
||||
} // namespace yaze
|
||||
57
src/app/platform/asset_loader.h
Normal file
57
src/app/platform/asset_loader.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef YAZE_APP_PLATFORM_ASSET_LOADER_H_
|
||||
#define YAZE_APP_PLATFORM_ASSET_LOADER_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
/**
|
||||
* @class AssetLoader
|
||||
* @brief Cross-platform asset file loading utility
|
||||
*
|
||||
* Handles platform-specific paths for loading assets from:
|
||||
* - macOS bundle resources
|
||||
* - Windows relative paths
|
||||
* - Linux relative paths
|
||||
* - Development build directories
|
||||
*/
|
||||
class AssetLoader {
|
||||
public:
|
||||
/**
|
||||
* Load a text file from the assets directory
|
||||
* @param relative_path Path relative to assets/ (e.g., "agent/system_prompt.txt")
|
||||
* @return File contents or error
|
||||
*/
|
||||
static absl::StatusOr<std::string> LoadTextFile(const std::string& relative_path);
|
||||
|
||||
/**
|
||||
* Find an asset file by trying multiple platform-specific paths
|
||||
* @param relative_path Path relative to assets/
|
||||
* @return Full path to file or error
|
||||
*/
|
||||
static absl::StatusOr<std::filesystem::path> FindAssetFile(const std::string& relative_path);
|
||||
|
||||
/**
|
||||
* Get list of search paths for a given asset
|
||||
* @param relative_path Path relative to assets/
|
||||
* @return Vector of paths to try in order
|
||||
*/
|
||||
static std::vector<std::filesystem::path> GetSearchPaths(const std::string& relative_path);
|
||||
|
||||
/**
|
||||
* Check if an asset file exists
|
||||
* @param relative_path Path relative to assets/
|
||||
* @return true if file exists in any search path
|
||||
*/
|
||||
static bool AssetExists(const std::string& relative_path);
|
||||
};
|
||||
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_ASSET_LOADER_H_
|
||||
303
src/app/platform/file_dialog.mm
Normal file
303
src/app/platform/file_dialog.mm
Normal file
@@ -0,0 +1,303 @@
|
||||
#include "util/file_util.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app/core/features.h"
|
||||
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
#include <nfd.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
/* Apple OSX and iOS (Darwin). */
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1
|
||||
/* iOS in Xcode simulator */
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
|
||||
#include "app/platform/app_delegate.h"
|
||||
|
||||
namespace {
|
||||
static std::string selectedFile;
|
||||
|
||||
void ShowOpenFileDialogImpl(void (^completionHandler)(std::string)) {
|
||||
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||
[appDelegate PresentDocumentPickerWithCompletionHandler:^(NSString *filePath) {
|
||||
selectedFile = std::string([filePath UTF8String]);
|
||||
completionHandler(selectedFile);
|
||||
}];
|
||||
}
|
||||
|
||||
std::string ShowOpenFileDialogSync() {
|
||||
__block std::string result;
|
||||
|
||||
ShowOpenFileDialogImpl(^(std::string filePath) {
|
||||
result = filePath;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() { return ShowOpenFileDialogSync(); }
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() { return ""; }
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetFilesInFolder(
|
||||
const std::string &folder) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetSubdirectoriesInFolder(
|
||||
const std::string &folder) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string yaze::util::GetBundleResourcePath() {
|
||||
NSBundle* bundle = [NSBundle mainBundle];
|
||||
NSString* resourceDirectoryPath = [bundle bundlePath];
|
||||
NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
|
||||
return [path UTF8String];
|
||||
}
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
/* macOS */
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogBespoke() {
|
||||
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
|
||||
[openPanel setCanChooseFiles:YES];
|
||||
[openPanel setCanChooseDirectories:NO];
|
||||
[openPanel setAllowsMultipleSelection:NO];
|
||||
|
||||
if ([openPanel runModal] == NSModalResponseOK) {
|
||||
NSURL* url = [[openPanel URLs] objectAtIndex:0];
|
||||
NSString* path = [url path];
|
||||
return std::string([path UTF8String]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
NSSavePanel* savePanel = [NSSavePanel savePanel];
|
||||
|
||||
if (!default_name.empty()) {
|
||||
[savePanel setNameFieldStringValue:[NSString stringWithUTF8String:default_name.c_str()]];
|
||||
}
|
||||
|
||||
if (!default_extension.empty()) {
|
||||
NSString* ext = [NSString stringWithUTF8String:default_extension.c_str()];
|
||||
[savePanel setAllowedFileTypes:@[ext]];
|
||||
}
|
||||
|
||||
if ([savePanel runModal] == NSModalResponseOK) {
|
||||
NSURL* url = [savePanel URL];
|
||||
NSString* path = [url path];
|
||||
return std::string([path UTF8String]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// Global feature flag-based dispatch methods
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowOpenFileDialogNFD();
|
||||
} else {
|
||||
return ShowOpenFileDialogBespoke();
|
||||
}
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowOpenFolderDialogNFD();
|
||||
} else {
|
||||
return ShowOpenFolderDialogBespoke();
|
||||
}
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowSaveFileDialogNFD(default_name, default_extension);
|
||||
} else {
|
||||
return ShowSaveFileDialogBespoke(default_name, default_extension);
|
||||
}
|
||||
}
|
||||
|
||||
// NFD implementation for macOS (fallback to bespoke if NFD not available)
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialogNFD() {
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
NFD_Init();
|
||||
nfdu8char_t *out_path = NULL;
|
||||
nfdu8filteritem_t filters[1] = {{"Rom File", "sfc,smc"}};
|
||||
nfdopendialogu8args_t args = {0};
|
||||
args.filterList = filters;
|
||||
args.filterCount = 1;
|
||||
|
||||
nfdresult_t result = NFD_OpenDialogU8_With(&out_path, &args);
|
||||
if (result == NFD_OKAY) {
|
||||
std::string file_path(out_path);
|
||||
NFD_FreePath(out_path);
|
||||
NFD_Quit();
|
||||
return file_path;
|
||||
} else if (result == NFD_CANCEL) {
|
||||
NFD_Quit();
|
||||
return "";
|
||||
}
|
||||
NFD_Quit();
|
||||
return "";
|
||||
#else
|
||||
// NFD not compiled in, use bespoke
|
||||
return ShowOpenFileDialogBespoke();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialogNFD() {
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
NFD_Init();
|
||||
nfdu8char_t *out_path = NULL;
|
||||
nfdresult_t result = NFD_PickFolderU8(&out_path, NULL);
|
||||
|
||||
if (result == NFD_OKAY) {
|
||||
std::string folder_path(out_path);
|
||||
NFD_FreePath(out_path);
|
||||
NFD_Quit();
|
||||
return folder_path;
|
||||
} else if (result == NFD_CANCEL) {
|
||||
NFD_Quit();
|
||||
return "";
|
||||
}
|
||||
NFD_Quit();
|
||||
return "";
|
||||
#else
|
||||
// NFD not compiled in, use bespoke
|
||||
return ShowOpenFolderDialogBespoke();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogNFD(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
#if defined(YAZE_ENABLE_NFD) && YAZE_ENABLE_NFD
|
||||
NFD_Init();
|
||||
nfdu8char_t *out_path = NULL;
|
||||
|
||||
nfdsavedialogu8args_t args = {0};
|
||||
if (!default_extension.empty()) {
|
||||
// Create filter for the save dialog
|
||||
static nfdu8filteritem_t filters[3] = {
|
||||
{"Theme File", "theme"},
|
||||
{"Project File", "yaze"},
|
||||
{"ROM File", "sfc,smc"}
|
||||
};
|
||||
|
||||
if (default_extension == "theme") {
|
||||
args.filterList = &filters[0];
|
||||
args.filterCount = 1;
|
||||
} else if (default_extension == "yaze") {
|
||||
args.filterList = &filters[1];
|
||||
args.filterCount = 1;
|
||||
} else if (default_extension == "sfc" || default_extension == "smc") {
|
||||
args.filterList = &filters[2];
|
||||
args.filterCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!default_name.empty()) {
|
||||
args.defaultName = default_name.c_str();
|
||||
}
|
||||
|
||||
nfdresult_t result = NFD_SaveDialogU8_With(&out_path, &args);
|
||||
if (result == NFD_OKAY) {
|
||||
std::string file_path(out_path);
|
||||
NFD_FreePath(out_path);
|
||||
NFD_Quit();
|
||||
return file_path;
|
||||
} else if (result == NFD_CANCEL) {
|
||||
NFD_Quit();
|
||||
return "";
|
||||
}
|
||||
NFD_Quit();
|
||||
return "";
|
||||
#else
|
||||
// NFD not compiled in, use bespoke
|
||||
return ShowSaveFileDialogBespoke(default_name, default_extension);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialogBespoke() {
|
||||
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
|
||||
[openPanel setCanChooseFiles:NO];
|
||||
[openPanel setCanChooseDirectories:YES];
|
||||
[openPanel setAllowsMultipleSelection:NO];
|
||||
|
||||
if ([openPanel runModal] == NSModalResponseOK) {
|
||||
NSURL* url = [[openPanel URLs] objectAtIndex:0];
|
||||
NSString* path = [url path];
|
||||
return std::string([path UTF8String]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetFilesInFolder(
|
||||
const std::string& folder) {
|
||||
std::vector<std::string> filenames;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSDirectoryEnumerator* enumerator =
|
||||
[fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
|
||||
NSString* file;
|
||||
while (file = [enumerator nextObject]) {
|
||||
if ([file hasPrefix:@"."]) {
|
||||
continue;
|
||||
}
|
||||
filenames.push_back(std::string([file UTF8String]));
|
||||
}
|
||||
return filenames;
|
||||
}
|
||||
|
||||
std::vector<std::string> yaze::util::FileDialogWrapper::GetSubdirectoriesInFolder(
|
||||
const std::string& folder) {
|
||||
std::vector<std::string> subdirectories;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSDirectoryEnumerator* enumerator =
|
||||
[fileManager enumeratorAtPath:[NSString stringWithUTF8String:folder.c_str()]];
|
||||
NSString* file;
|
||||
while (file = [enumerator nextObject]) {
|
||||
if ([file hasPrefix:@"."]) {
|
||||
continue;
|
||||
}
|
||||
BOOL isDirectory;
|
||||
NSString* path =
|
||||
[NSString stringWithFormat:@"%@/%@", [NSString stringWithUTF8String:folder.c_str()], file];
|
||||
[fileManager fileExistsAtPath:path isDirectory:&isDirectory];
|
||||
if (isDirectory) {
|
||||
subdirectories.push_back(std::string([file UTF8String]));
|
||||
}
|
||||
}
|
||||
return subdirectories;
|
||||
}
|
||||
|
||||
std::string yaze::util::GetBundleResourcePath() {
|
||||
NSBundle* bundle = [NSBundle mainBundle];
|
||||
NSString* resourceDirectoryPath = [bundle bundlePath];
|
||||
NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"];
|
||||
return [path UTF8String];
|
||||
}
|
||||
|
||||
#else
|
||||
// Unsupported platform
|
||||
#endif // TARGET_OS_MAC
|
||||
|
||||
#endif // __APPLE__ && __MACH__
|
||||
140
src/app/platform/font_loader.cc
Normal file
140
src/app/platform/font_loader.cc
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "app/platform/font_loader.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "util/file_util.h"
|
||||
#include "app/gui/icons.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
static const char* KARLA_REGULAR = "Karla-Regular.ttf";
|
||||
static const char* ROBOTO_MEDIUM = "Roboto-Medium.ttf";
|
||||
static const char* COUSINE_REGULAR = "Cousine-Regular.ttf";
|
||||
static const char* DROID_SANS = "DroidSans.ttf";
|
||||
static const char* NOTO_SANS_JP = "NotoSansJP.ttf";
|
||||
static const char* IBM_PLEX_JP = "IBMPlexSansJP-Bold.ttf";
|
||||
|
||||
static const float FONT_SIZE_DEFAULT = 16.0F;
|
||||
static const float FONT_SIZE_DROID_SANS = 18.0F;
|
||||
static const float ICON_FONT_SIZE = 18.0F;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string SetFontPath(const std::string& font_path) {
|
||||
#ifdef __APPLE__
|
||||
#if TARGET_OS_IOS == 1
|
||||
const std::string kBundlePath = util::GetBundleResourcePath();
|
||||
return kBundlePath + font_path;
|
||||
#else
|
||||
return absl::StrCat(util::GetBundleResourcePath(), "Contents/Resources/font/",
|
||||
font_path);
|
||||
#endif
|
||||
#else
|
||||
return absl::StrCat("assets/font/", font_path);
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status LoadFont(const FontConfig& font_config) {
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
std::string actual_font_path = SetFontPath(font_config.font_path);
|
||||
// Check if the file exists with std library first, since ImGui IO will assert
|
||||
// if the file does not exist
|
||||
if (!std::filesystem::exists(actual_font_path)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Font file %s does not exist", actual_font_path));
|
||||
}
|
||||
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(actual_font_path.data(),
|
||||
font_config.font_size)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to load font from %s", actual_font_path));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status AddIconFont(const FontConfig& /*config*/) {
|
||||
static const ImWchar icons_ranges[] = {ICON_MIN_MD, 0xf900, 0};
|
||||
ImFontConfig icons_config{};
|
||||
icons_config.MergeMode = true;
|
||||
icons_config.GlyphOffset.y = 5.0F;
|
||||
icons_config.GlyphMinAdvanceX = 13.0F;
|
||||
icons_config.PixelSnapH = true;
|
||||
std::string icon_font_path = SetFontPath(FONT_ICON_FILE_NAME_MD);
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(icon_font_path.c_str(), ICON_FONT_SIZE,
|
||||
&icons_config, icons_ranges)) {
|
||||
return absl::InternalError("Failed to add icon fonts");
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status AddJapaneseFont(const FontConfig& /*config*/) {
|
||||
ImFontConfig japanese_font_config{};
|
||||
japanese_font_config.MergeMode = true;
|
||||
japanese_font_config.GlyphOffset.y = 5.0F;
|
||||
japanese_font_config.GlyphMinAdvanceX = 13.0F;
|
||||
japanese_font_config.PixelSnapH = true;
|
||||
std::string japanese_font_path = SetFontPath(NOTO_SANS_JP);
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(japanese_font_path.data(), ICON_FONT_SIZE,
|
||||
&japanese_font_config,
|
||||
imgui_io.Fonts->GetGlyphRangesJapanese())) {
|
||||
return absl::InternalError("Failed to add Japanese fonts");
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::Status LoadPackageFonts() {
|
||||
if (font_registry.fonts.empty()) {
|
||||
// Initialize the font names and sizes with proper ImFontConfig initialization
|
||||
font_registry.fonts = {
|
||||
FontConfig{KARLA_REGULAR, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{ROBOTO_MEDIUM, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{COUSINE_REGULAR, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{IBM_PLEX_JP, FONT_SIZE_DEFAULT, {}, {}},
|
||||
FontConfig{DROID_SANS, FONT_SIZE_DROID_SANS, {}, {}},
|
||||
};
|
||||
}
|
||||
|
||||
// Load fonts with associated icon and Japanese merges
|
||||
for (const auto& font_config : font_registry.fonts) {
|
||||
RETURN_IF_ERROR(LoadFont(font_config));
|
||||
RETURN_IF_ERROR(AddIconFont(font_config));
|
||||
RETURN_IF_ERROR(AddJapaneseFont(font_config));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ReloadPackageFont(const FontConfig& config) {
|
||||
ImGuiIO& imgui_io = ImGui::GetIO();
|
||||
std::string actual_font_path = SetFontPath(config.font_path);
|
||||
if (!imgui_io.Fonts->AddFontFromFileTTF(actual_font_path.data(),
|
||||
config.font_size)) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to load font from %s", actual_font_path));
|
||||
}
|
||||
RETURN_IF_ERROR(AddIconFont(config));
|
||||
RETURN_IF_ERROR(AddJapaneseFont(config));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
void LoadSystemFonts() {
|
||||
// Load Linux System Fonts into ImGui
|
||||
// System font loading is now handled by NFD (Native File Dialog)
|
||||
// This function is kept for compatibility but does nothing
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace yaze
|
||||
34
src/app/platform/font_loader.h
Normal file
34
src/app/platform/font_loader.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef YAZE_APP_PLATFORM_FONTLOADER_H
|
||||
#define YAZE_APP_PLATFORM_FONTLOADER_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
struct FontConfig {
|
||||
const char* font_path;
|
||||
float font_size;
|
||||
ImFontConfig im_font_config;
|
||||
ImFontConfig jp_conf_config;
|
||||
};
|
||||
|
||||
struct FontState {
|
||||
std::vector<FontConfig> fonts;
|
||||
};
|
||||
|
||||
static FontState font_registry;
|
||||
|
||||
absl::Status LoadPackageFonts();
|
||||
|
||||
absl::Status ReloadPackageFont(const FontConfig& config);
|
||||
|
||||
void LoadSystemFonts();
|
||||
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_PLATFORM_FONTLOADER_H
|
||||
59
src/app/platform/font_loader.mm
Normal file
59
src/app/platform/font_loader.mm
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "app/platform/font_loader.h"
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#include "app/gui/icons.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
#if TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1
|
||||
/* iOS */
|
||||
void yaze::LoadSystemFonts() {}
|
||||
|
||||
#elif TARGET_OS_MAC == 1
|
||||
/* macOS */
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
void yaze::LoadSystemFonts() {
|
||||
NSArray *fontNames = @[ @"Helvetica", @"Times New Roman", @"Courier", @"Arial", @"Verdana" ];
|
||||
|
||||
for (NSString *fontName in fontNames) {
|
||||
NSFont *font = [NSFont fontWithName:fontName size:14.0];
|
||||
if (!font) {
|
||||
NSLog(@"Font not found: %@", fontName);
|
||||
continue;
|
||||
}
|
||||
|
||||
CTFontDescriptorRef fontDescriptor =
|
||||
CTFontDescriptorCreateWithNameAndSize((CFStringRef)font.fontName, font.pointSize);
|
||||
CFURLRef fontURL = (CFURLRef)CTFontDescriptorCopyAttribute(fontDescriptor, kCTFontURLAttribute);
|
||||
NSString *fontPath = [(NSURL *)fontURL path];
|
||||
CFRelease(fontDescriptor);
|
||||
|
||||
if (fontPath != nil && [[NSFileManager defaultManager] isReadableFileAtPath:fontPath]) {
|
||||
// Load the font into ImGui
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImFontConfig icons_config;
|
||||
icons_config.MergeMode = true;
|
||||
icons_config.GlyphOffset.y = 5.0f;
|
||||
icons_config.GlyphMinAdvanceX = 13.0f;
|
||||
icons_config.PixelSnapH = true;
|
||||
static const ImWchar icons_ranges[] = {ICON_MIN_MD, 0xf900, 0};
|
||||
static const float ICON_FONT_SIZE = 18.0f;
|
||||
ImFont *imFont = io.Fonts->AddFontFromFileTTF([fontPath UTF8String], 14.0f);
|
||||
if (!imFont) {
|
||||
NSLog(@"Failed to load font: %@", fontPath);
|
||||
}
|
||||
io.Fonts->AddFontFromFileTTF(FONT_ICON_FILE_NAME_MD, ICON_FONT_SIZE, &icons_config,
|
||||
icons_ranges);
|
||||
} else {
|
||||
NSLog(@"Font file not accessible: %@", fontPath);
|
||||
}
|
||||
|
||||
if (fontURL) {
|
||||
CFRelease(fontURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
35
src/app/platform/view_controller.h
Normal file
35
src/app/platform/view_controller.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef YAZE_APP_CORE_PLATFORM_VIEW_CONTROLLER_H
|
||||
#define YAZE_APP_CORE_PLATFORM_VIEW_CONTROLLER_H
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#ifdef __OBJC__
|
||||
@interface AppViewController : NSViewController <NSWindowDelegate>
|
||||
@property(nonatomic) yaze::core::Controller *controller;
|
||||
@end
|
||||
#endif
|
||||
#else
|
||||
#ifdef __OBJC__
|
||||
@interface AppViewController : UIViewController <MTKViewDelegate>
|
||||
@property(nonatomic) yaze::core::Controller *controller;
|
||||
@property(nonatomic) UIHoverGestureRecognizer *hoverGestureRecognizer;
|
||||
@property(nonatomic) UIPinchGestureRecognizer *pinchRecognizer;
|
||||
@property(nonatomic) UISwipeGestureRecognizer *swipeRecognizer;
|
||||
@property(nonatomic) UILongPressGestureRecognizer *longPressRecognizer;
|
||||
@end
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __OBJC__
|
||||
@interface AppViewController () <MTKViewDelegate>
|
||||
@property(nonatomic, readonly) MTKView *mtkView;
|
||||
@property(nonatomic, strong) id<MTLDevice> device;
|
||||
@property(nonatomic, strong) id<MTLCommandQueue> commandQueue;
|
||||
@end
|
||||
#endif
|
||||
|
||||
#endif // __APPLE__
|
||||
|
||||
#endif // YAZE_APP_CORE_PLATFORM_VIEW_CONTROLLER_H
|
||||
Reference in New Issue
Block a user