⛏️ index : haiku.git

/*
 * Copyright 2016, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT license.
 *
 * Authors:
 *		Augustin Cavalier <waddlesplash>
 *		kerwizzy
 */


#include <AboutWindow.h>
#include <Application.h>
#include <Bitmap.h>
#include <BitmapStream.h>
#include <String.h>
#include <Catalog.h>
#include <Directory.h>
#include <File.h>
#include <FilePanel.h>
#include <FindDirectory.h>
#include <MenuBar.h>
#include <NodeInfo.h>
#include <Path.h>
#include <TranslationUtils.h>
#include <TranslatorRoster.h>
#include <LayoutBuilder.h>
#include <View.h>
#include <Window.h>
#include <Screen.h>
#include <ScrollView.h>

#include <algorithm>

#include "FractalEngine.h"

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "MandelbrotWindow"

#define MANDELBROT_VIEW_REFRESH_FPS 10

// #pragma mark - FractalView

//#define TRACE_MANDELBROT_VIEW
#ifdef TRACE_MANDELBROT_VIEW
#	include <stdio.h>
#	define TRACE(x...) printf(x)
#else
#	define TRACE(x...)
#endif


class FractalView : public BView {
public:
	FractalView();
	~FractalView();

	virtual void AttachedToWindow();
	virtual void FrameResized(float, float);
	virtual void Pulse();

	virtual void MouseDown(BPoint where);
	virtual void MouseMoved(BPoint where, uint32 mode, const BMessage*);
	virtual void MouseUp(BPoint where);

	virtual void MessageReceived(BMessage* msg);
	virtual void Draw(BRect updateRect);

			void ResetPosition();
			void SetLocationFromFrame(double frameX, double frameY);
			void ZoomFractal(double originX, double originY, double zoomFactor);
			void ZoomFractalFromFrame(double frameOriginX, double frameOriginY,
				double zoomFactor);
			void ImportBitsAndInvalidate();
			void RedrawFractal();
			void UpdateSize();
			void CreateDisplayBitmap(uint16 width, uint16 height);

			void StartSave();
			void WriteImage(entry_ref*, char*);
			void EndSave();

			FractalEngine* fFractalEngine;
	enum {
		MSG_START_SAVE,
		MSG_WRITE_IMAGE
	};

private:
			BRect GetDragFrame();

	BPoint fSelectStart;
	BPoint fSelectEnd;
	bool fSelecting;
	uint32 fMouseButtons;

	BBitmap* fDisplayBitmap;

	double fLocationX;
	double fLocationY;
	double fSize;

	BFilePanel* fSavePanel;

	bool fSaving;
};


FractalView::FractalView()
	:
	BView(NULL, B_WILL_DRAW | B_FRAME_EVENTS | B_PULSE_NEEDED),
	fFractalEngine(NULL),
	fSelecting(false),
	fDisplayBitmap(NULL),
	fLocationX(0),
	fLocationY(0),
	fSize(0.005),
	fSavePanel(NULL),
	fSaving(false)
{
	SetHighColor(make_color(255, 255, 255, 255));
}


FractalView::~FractalView()
{
	delete fDisplayBitmap;
}


void FractalView::ResetPosition()
{
	fLocationX = 0;
	fLocationY = 0;
	fSize = 0.005;
}


void FractalView::AttachedToWindow()
{
	fFractalEngine = new FractalEngine(this, Window());
	fFractalEngine->Run();
	TRACE("Attached to window\n");
}


void FractalView::FrameResized(float, float)
{
	TRACE("Frame Resize\n");
	UpdateSize();
}


void FractalView::UpdateSize()
{
	TRACE("Update Size\n");
	BMessage msg(FractalEngine::MSG_RESIZE);

	uint16 width = (uint16)Frame().Width();
	uint16 height = (uint16)Frame().Height();

	msg.AddUInt16("width", width);
	msg.AddUInt16("height", height);

	CreateDisplayBitmap(width, height);

	msg.AddPointer("bitmap", fDisplayBitmap);

	fFractalEngine->PostMessage(&msg); // Create the new buffer
}


void FractalView::CreateDisplayBitmap(uint16 width,uint16 height)
{
	delete fDisplayBitmap;
	fDisplayBitmap = NULL;
	TRACE("width %u height %u\n",width,height);
	BRect rect(0, 0, width, height);
	fDisplayBitmap = new BBitmap(rect, B_RGB24);
}


BRect FractalView::GetDragFrame()
{
	BRect dragZone = BRect(std::min(fSelectStart.x, fSelectEnd.x),
		std::min(fSelectStart.y, fSelectEnd.y),
		std::max(fSelectStart.x, fSelectEnd.x),
		std::max(fSelectStart.y, fSelectEnd.y)),
		frame = Frame();
	float width = dragZone.Width(),
		height = width * (frame.Height() / frame.Width());

	float x1 = fSelectStart.x, y1 = fSelectStart.y,	x2, y2;
	if (fSelectStart.x < fSelectEnd.x)
		x2 = x1 + width;
	else
		x2 = x1 - width;
	if (fSelectStart.y < fSelectEnd.y)
		y2 = y1 + height;
	else
		y2 = y1 - height;
	return BRect(x1, y1, x2, y2);
}


void FractalView::MouseDown(BPoint where)
{
	fSelecting = true;
	fSelectStart = where;
	fMouseButtons = 0;
	Window()->CurrentMessage()->FindInt32("buttons", (int32*)&fMouseButtons);
}


void FractalView::MouseMoved(BPoint where, uint32 mode, const BMessage*)
{
	if (fSelecting) {
		fSelectEnd = where;
		Invalidate();
	}
}


void FractalView::SetLocationFromFrame(double frameX,double frameY)
{
	BRect frame = Frame();

	fLocationX = ((frameX - frame.Width() / 2) * fSize + fLocationX);
	fLocationY = ((frameY - frame.Height() / 2) * -fSize + fLocationY);
		// -fSize because is in raster coordinates (y swapped)
}


void FractalView::ZoomFractalFromFrame(double frameOriginX, double frameOriginY,
	double zoomFactor)
{
	BRect frame = Frame();

	ZoomFractal((frameOriginX - frame.Width() / 2) * fSize + fLocationX,
		 (frameOriginY - frame.Height() / 2) * -fSize + fLocationY,
		 zoomFactor);
}


void FractalView::ZoomFractal(double originX, double originY, double zoomFactor)
{
	double deltaX = originX - fLocationX;
	double deltaY = originY - fLocationY;

	TRACE("oX %g oY %g zoom %g\n", originX, originY, zoomFactor);

	deltaX /= zoomFactor;
	deltaY /= zoomFactor;

	fLocationX = originX - deltaX;
	fLocationY = originY - deltaY;
	fSize /= zoomFactor;
}


void FractalView::MouseUp(BPoint where)
{
	BRect frame = Frame();
	fSelecting = false;
	if (fabs(fSelectStart.x - where.x) > 4) {
		fSelectEnd = where;
		BRect dragFrame = GetDragFrame();
		BPoint lt = dragFrame.LeftTop();
		float centerX = lt.x + dragFrame.Width() / 2,
			centerY = lt.y + dragFrame.Height() / 2;

		SetLocationFromFrame(centerX, centerY);
		fSize = std::fabs((dragFrame.Width() * fSize) / frame.Width());
	} else {
		if (fMouseButtons & B_PRIMARY_MOUSE_BUTTON) {
			SetLocationFromFrame(where.x, where.y);
			ZoomFractal(fLocationX, fLocationY, 2);
		} else {
			ZoomFractal(fLocationX, fLocationY, 0.5);
		}
	}
	RedrawFractal();
}


void FractalView::MessageReceived(BMessage* msg)
{
	switch (msg->what) {
	case B_MOUSE_WHEEL_CHANGED: {
		float change = msg->FindFloat("be:wheel_delta_y");
		BPoint where;
		GetMouse(&where, NULL);
		double zoomFactor;
		if (change < 0)
			zoomFactor = 3.0/2.0;
		else
			zoomFactor = 2.0/3.0;
		ZoomFractalFromFrame(where.x, where.y, zoomFactor);

		RedrawFractal();
		break;
	}

	case FractalEngine::MSG_BUFFER_CREATED:
		TRACE("Got buffer created msg.\n");

		ImportBitsAndInvalidate();
		RedrawFractal();
		break;

	case FractalEngine::MSG_RENDER_COMPLETE:
		TRACE("Got render complete msg.\n");

		Window()->SetPulseRate(0);
		ImportBitsAndInvalidate();
		break;

	case MSG_WRITE_IMAGE: {
		delete fSavePanel;
		fSavePanel = NULL;

		entry_ref dirRef;
		char* name;
		msg->FindRef("directory", &dirRef);
		msg->FindString((const char*)"name", (const char**) &name);

		WriteImage(&dirRef, name);
		break;
	}

	case B_CANCEL:
		//	image is frozen before the FilePanel is shown
		EndSave();
		break;

	default:
		BView::MessageReceived(msg);
		break;
	}
}


void FractalView::Pulse()
{
	ImportBitsAndInvalidate();
}


void FractalView::ImportBitsAndInvalidate()
{
	if (fSaving) {
		TRACE("Not importing bits because saving.\n");
		return;
	}
	TRACE("Importing bits...\n");

	fFractalEngine->WriteToBitmap(fDisplayBitmap);
	Invalidate();
}


void FractalView::RedrawFractal()
{
	Window()->SetPulseRate(1000000 / MANDELBROT_VIEW_REFRESH_FPS);
	BMessage message(FractalEngine::MSG_RENDER);
	message.AddDouble("locationX", fLocationX);
	message.AddDouble("locationY", fLocationY);
	message.AddDouble("size", fSize);
	fFractalEngine->PostMessage(&message);
}


void FractalView::Draw(BRect updateRect)
{
	DrawBitmap(fDisplayBitmap, updateRect, updateRect);
	if (fSelecting)
		StrokeRect(GetDragFrame());
}


void FractalView::StartSave() {
	TRACE("Got to start save\n");
	fSaving = true;

	BMessenger messenger(this);
	BMessage message(MSG_WRITE_IMAGE);
	fSavePanel = new BFilePanel(B_SAVE_PANEL, &messenger, 0, 0, false,
		&message);
	BString* filename = new BString();
	filename->SetToFormat("%g-%g-%g.png", fLocationX, fLocationY, fSize);

	fSavePanel->SetSaveText(filename->String());
	fSavePanel->Show();
}


void FractalView::WriteImage(entry_ref* dirRef, char* name)
{
	TRACE("Got to write save handler\n");

	BFile file;
	BDirectory parentDir(dirRef);
	parentDir.CreateFile(name, &file);

	// Write the screenshot bitmap to the file
	BBitmapStream stream(fDisplayBitmap);
	BTranslatorRoster* roster = BTranslatorRoster::Default();
	roster->Translate(&stream, NULL, NULL, &file, B_PNG_FORMAT,
		B_TRANSLATOR_BITMAP);

	BNodeInfo info(&file);
	if (info.InitCheck() == B_OK)
		info.SetType("image/png");

	BBitmap* bitmap;
	stream.DetachBitmap(&bitmap);
	// The stream takes over ownership of the bitmap

	// unfreeze the image, image was frozen before invoke of FilePanel
	EndSave();
}


void FractalView::EndSave()
{
	fSaving = false;
	ImportBitsAndInvalidate();
}


// #pragma mark - MandelbrotWindow


class MandelbrotWindow : public BWindow
{
public:
	enum {
		MSG_MANDELBROT_SET = 'MndW',
		MSG_BURNINGSHIP_SET,
		MSG_TRICORN_SET,
		MSG_JULIA_SET,
		MSG_ORBITTRAP_SET,
		MSG_MULTIBROT_SET,

		MSG_ROYAL_PALETTE,
		MSG_DEEPFROST_PALETTE,
		MSG_FROST_PALETTE,
		MSG_FIRE_PALETTE,
		MSG_MIDNIGHT_PALETTE,
		MSG_GRASSLAND_PALETTE,
		MSG_LIGHTNING_PALETTE,
		MSG_SPRING_PALETTE,
		MSG_HIGHCONTRAST_PALETTE,

		MSG_ITER_128,
		MSG_ITER_512,
		MSG_ITER_1024,
		MSG_ITER_4096,
		MSG_ITER_8192,
		MSG_ITER_12288,
		MSG_ITER_16384,

		MSG_SUBSAMPLING_1,
		MSG_SUBSAMPLING_2,
		MSG_SUBSAMPLING_3,
		MSG_SUBSAMPLING_4,

		MSG_TOGGLE_FULLSCREEN
	};
				MandelbrotWindow(BRect frame);
				~MandelbrotWindow() {}

	void ToggleFullscreen();

	virtual void DispatchMessage(BMessage* message, BHandler* target);
	virtual void MessageReceived(BMessage* msg);
	virtual bool QuitRequested();

	bool fFullScreen;

	BMenuBar* fMenuBar;
	BRect fWindowFrame;

private:
		FractalView* fFractalView;
};


MandelbrotWindow::MandelbrotWindow(BRect frame)
	:
	BWindow(frame, B_TRANSLATE_SYSTEM_NAME("Mandelbrot"), B_TITLED_WINDOW_LOOK,
		B_NORMAL_WINDOW_FEEL, 0L),
	fFractalView(new FractalView)
{
	fFullScreen = false;
	fMenuBar = new BMenuBar("MenuBar");
	BMenu* setMenu;
	BMenu* paletteMenu;
	BMenu* iterMenu;
	BMenu* subsamplingMenu;
	BLayoutBuilder::Menu<>(fMenuBar)
		.AddMenu(B_TRANSLATE("File"))
			.AddItem(B_TRANSLATE("Save as image" B_UTF8_ELLIPSIS), 
				FractalView::MSG_START_SAVE, 'S')
			.AddSeparator()
			.AddItem(B_TRANSLATE("About"), B_ABOUT_REQUESTED)
			.AddItem(B_TRANSLATE("Quit"), B_QUIT_REQUESTED, 'Q')
		.End()
		.AddMenu(B_TRANSLATE("View"))
			.AddItem(B_TRANSLATE("Full screen"), MSG_TOGGLE_FULLSCREEN,
				B_RETURN)
		.End()
		.AddMenu(B_TRANSLATE("Set"))
			.GetMenu(setMenu)
			.AddItem(B_TRANSLATE("Mandelbrot"), MSG_MANDELBROT_SET)
			.AddItem(B_TRANSLATE("Burning Ship"), MSG_BURNINGSHIP_SET)
			.AddItem(B_TRANSLATE("Tricorn"), MSG_TRICORN_SET)
			.AddItem(B_TRANSLATE("Julia"), MSG_JULIA_SET)
			.AddItem(B_TRANSLATE("Orbit Trap"), MSG_ORBITTRAP_SET)
			.AddItem(B_TRANSLATE("Multibrot"), MSG_MULTIBROT_SET)
		.End()
		.AddMenu(B_TRANSLATE("Palette"))
			.GetMenu(paletteMenu)
			.AddItem(B_TRANSLATE("Royal"), MSG_ROYAL_PALETTE)
			.AddItem(B_TRANSLATE("Deepfrost"), MSG_DEEPFROST_PALETTE)
			.AddItem(B_TRANSLATE("Frost"), MSG_FROST_PALETTE)
			.AddItem(B_TRANSLATE("Fire"), MSG_FIRE_PALETTE)
			.AddItem(B_TRANSLATE("Midnight"), MSG_MIDNIGHT_PALETTE)
			.AddItem(B_TRANSLATE("Grassland"), MSG_GRASSLAND_PALETTE)
			.AddItem(B_TRANSLATE("Lightning"), MSG_LIGHTNING_PALETTE)
			.AddItem(B_TRANSLATE("Spring"), MSG_SPRING_PALETTE)
			.AddItem(B_TRANSLATE("High contrast"), MSG_HIGHCONTRAST_PALETTE)
		.End()
		.AddMenu(B_TRANSLATE("Iterations"))
			.GetMenu(iterMenu)
			.AddItem("128", MSG_ITER_128)
			.AddItem("512", MSG_ITER_512)
			.AddItem("1024", MSG_ITER_1024)
			.AddItem("4096", MSG_ITER_4096)
			.AddItem("8192", MSG_ITER_8192)
			.AddItem("12288", MSG_ITER_12288)
			.AddItem("16384", MSG_ITER_16384)
		.End()
		.AddMenu(B_TRANSLATE("Subsampling"))
			.GetMenu(subsamplingMenu)
			.AddItem(B_TRANSLATE("1 (none)"), MSG_SUBSAMPLING_1)
			.AddItem("4", MSG_SUBSAMPLING_2)
			.AddItem("9", MSG_SUBSAMPLING_3)
			.AddItem("16", MSG_SUBSAMPLING_4)
		.End()
	.End();
	setMenu->SetRadioMode(true);
	setMenu->FindItem(MSG_MANDELBROT_SET)->SetMarked(true);
	paletteMenu->SetRadioMode(true);
	paletteMenu->FindItem(MSG_ROYAL_PALETTE)->SetMarked(true);
	iterMenu->SetRadioMode(true);
	iterMenu->FindItem(MSG_ITER_1024)->SetMarked(true);
	subsamplingMenu->SetRadioMode(true);
	subsamplingMenu->FindItem(MSG_SUBSAMPLING_2)->SetMarked(true);

	BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
		.SetInsets(0)
		.Add(fMenuBar)
		.Add(fFractalView)
	.End();
}

void
MandelbrotWindow::ToggleFullscreen() {
	BRect frame;
	fFullScreen = !fFullScreen;
	if (fFullScreen) {
		TRACE("Enabling fullscreen\n");
		BScreen screen;
		fWindowFrame = Frame();
		frame = screen.Frame();
		frame.top -= fMenuBar->Bounds().Height() + 1;

		SetFlags(Flags() | B_NOT_RESIZABLE | B_NOT_MOVABLE);

		Activate();
		// make the window frontmost
	} else {
		TRACE("Disabling fullscreen\n");
		frame = fWindowFrame;

		SetFlags(Flags() & ~(B_NOT_RESIZABLE | B_NOT_MOVABLE));
	}

	MoveTo(frame.left, frame.top);
	ResizeTo(frame.Width(), frame.Height());

	Layout(false);
}


#define HANDLE_SET(uiwhat, id) \
	case uiwhat: { \
		BMessage msg(FractalEngine::MSG_CHANGE_SET); \
		msg.AddUInt8("set", id); \
		fFractalView->fFractalEngine->PostMessage(&msg); \
		fFractalView->ResetPosition(); \
		fFractalView->RedrawFractal(); \
		break; \
	}
#define HANDLE_PALETTE(uiwhat, id) \
	case uiwhat: { \
		BMessage msg(FractalEngine::MSG_SET_PALETTE); \
		msg.AddUInt8("palette", id); \
		fFractalView->fFractalEngine->PostMessage(&msg); \
		fFractalView->RedrawFractal(); \
		break; \
	}
#define HANDLE_ITER(uiwhat, id) \
	case uiwhat: { \
		BMessage msg(FractalEngine::MSG_SET_ITERATIONS); \
		msg.AddUInt16("iterations", id); \
		fFractalView->fFractalEngine->PostMessage(&msg); \
		fFractalView->RedrawFractal(); \
		break; \
	}
#define HANDLE_SUBSAMPLING(uiwhat, id) \
	case uiwhat: { \
		BMessage msg(FractalEngine::MSG_SET_SUBSAMPLING); \
		msg.AddUInt8("subsampling", id); \
		fFractalView->fFractalEngine->PostMessage(&msg); \
		fFractalView->RedrawFractal(); \
		break; \
	}


void
MandelbrotWindow::DispatchMessage(BMessage* message, BHandler* target)
{
	const char* bytes;
	int32 modifierKeys;
	if ((message->what == B_KEY_DOWN || message->what == B_UNMAPPED_KEY_DOWN)
		&& message->FindString("bytes", &bytes) == B_OK
		&& message->FindInt32("modifiers", &modifierKeys) == B_OK) {
		if (bytes[0] == B_FUNCTION_KEY) {
			// Matches WebPositive fullscreen key (F11)
			int32 key;
			if (message->FindInt32("key", &key) == B_OK) {
				switch (key) {
					case B_F11_KEY: {
						ToggleFullscreen();
						break;
					}

					default:
						break;
				}
			}
		}
	}

	BWindow::DispatchMessage(message, target);
}


void
MandelbrotWindow::MessageReceived(BMessage* msg)
{
	switch (msg->what) {
	HANDLE_SET(MSG_MANDELBROT_SET, 0)
	HANDLE_SET(MSG_BURNINGSHIP_SET, 1)
	HANDLE_SET(MSG_TRICORN_SET, 2)
	HANDLE_SET(MSG_JULIA_SET, 3)
	HANDLE_SET(MSG_ORBITTRAP_SET, 4)
	HANDLE_SET(MSG_MULTIBROT_SET, 5)

	HANDLE_PALETTE(MSG_ROYAL_PALETTE, 0)
	HANDLE_PALETTE(MSG_DEEPFROST_PALETTE, 1)
	HANDLE_PALETTE(MSG_FROST_PALETTE, 2)
	HANDLE_PALETTE(MSG_FIRE_PALETTE, 3)
	HANDLE_PALETTE(MSG_MIDNIGHT_PALETTE, 4)
	HANDLE_PALETTE(MSG_GRASSLAND_PALETTE, 5)
	HANDLE_PALETTE(MSG_LIGHTNING_PALETTE, 6)
	HANDLE_PALETTE(MSG_SPRING_PALETTE, 7)
	HANDLE_PALETTE(MSG_HIGHCONTRAST_PALETTE, 8)

	HANDLE_ITER(MSG_ITER_128, 128)
	HANDLE_ITER(MSG_ITER_512, 512)
	HANDLE_ITER(MSG_ITER_1024, 1024)
	HANDLE_ITER(MSG_ITER_4096, 4096)
	HANDLE_ITER(MSG_ITER_8192, 8192)
	HANDLE_ITER(MSG_ITER_12288, 12288)
	HANDLE_ITER(MSG_ITER_16384, 16384)

	HANDLE_SUBSAMPLING(MSG_SUBSAMPLING_1, 1)
	HANDLE_SUBSAMPLING(MSG_SUBSAMPLING_2, 2)
	HANDLE_SUBSAMPLING(MSG_SUBSAMPLING_3, 3)
	HANDLE_SUBSAMPLING(MSG_SUBSAMPLING_4, 4)

	case FractalView::MSG_START_SAVE: {
		fFractalView->StartSave();
		break;
	}

	case MSG_TOGGLE_FULLSCREEN:
		ToggleFullscreen();
		break;

	case B_ABOUT_REQUESTED: {
		BAboutWindow* wind = new BAboutWindow("Mandelbrot",
			"application/x-vnd.Haiku-Mandelbrot");

		const char* authors[] = {
			"Augustin Cavalier <waddlesplash>",
			"kerwizzy",
			NULL
		};
		wind->AddCopyright(2016, "Haiku, Inc.");
		wind->AddAuthors(authors);
		wind->Show();
		break;
	}

	case B_KEY_DOWN: {
		int8 val;
		if (msg->FindInt8("byte", &val) == B_OK && val == B_ESCAPE
			&& fFullScreen)
			ToggleFullscreen();
		break;
	}

	default:
		BWindow::MessageReceived(msg);
		break;
	}
}
#undef HANDLE_SET
#undef HANDLE_PALETTE
#undef HANDLE_ITER
#undef HANDLE_SUBSAMPLING


bool
MandelbrotWindow::QuitRequested()
{
	if (BWindow::QuitRequested()) {
		be_app->PostMessage(B_QUIT_REQUESTED);
		return true;
	}
	return false;
}


// #pragma mark - MandelbrotApp


class MandelbrotApp : public BApplication
{
public:
				MandelbrotApp()
					: BApplication("application/x-vnd.Haiku-Mandelbrot") {}

		void	ReadyToRun();
		bool	QuitRequested() { return true; }
};


void
MandelbrotApp::ReadyToRun()
{
	MandelbrotWindow* wind = new MandelbrotWindow(BRect(0, 0, 640, 480));
	wind->CenterOnScreen();
	wind->Show();
}


int
main(int argc, char* argv[])
{
	MandelbrotApp().Run();
	return 0;
}