2
0
Эх сурвалжийг харах

BMP loading, show workspace icon files on startup panel

Brian Fiete 5 сар өмнө
parent
commit
05cda98c85

+ 2 - 0
BeefySysLib/BeefySysLib.vcxproj

@@ -463,6 +463,7 @@ copy /y "$(OutDir)$(TargetName).lib" "$(SolutionDir)\BeefLibs\Beefy2D\dist\"</Co
     <ClCompile Include="gfx\Texture.cpp" />
     <ClCompile Include="HeadlessApp.cpp" />
     <ClCompile Include="img\BFIData.cpp" />
+    <ClCompile Include="img\BMPData.cpp" />
     <ClCompile Include="img\ImageAdjustments.cpp" />
     <ClCompile Include="img\ImageData.cpp" />
     <ClCompile Include="img\ImageUtils.cpp" />
@@ -1985,6 +1986,7 @@ copy /y "$(OutDir)$(TargetName).lib" "$(SolutionDir)\BeefLibs\Beefy2D\dist\"</Co
     <ClInclude Include="gfx\Texture.h" />
     <ClInclude Include="HeadlessApp.h" />
     <ClInclude Include="img\BFIData.h" />
+    <ClInclude Include="img\BMPData.h" />
     <ClInclude Include="img\ImageAdjustments.h" />
     <ClInclude Include="img\ImageData.h" />
     <ClInclude Include="img\ImageUtils.h" />

+ 6 - 0
BeefySysLib/BeefySysLib.vcxproj.filters

@@ -740,6 +740,9 @@
     <ClCompile Include="util\MTRand.cpp">
       <Filter>src\util</Filter>
     </ClCompile>
+    <ClCompile Include="img\BMPData.cpp">
+      <Filter>src\img</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="Common.h">
@@ -1141,6 +1144,9 @@
     <ClInclude Include="util\MTRand.h">
       <Filter>src\util</Filter>
     </ClInclude>
+    <ClInclude Include="img\BMPData.h">
+      <Filter>src\img</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <CustomBuild Include="third_party\libffi\i686-pc-cygwin\src\x86\win32.asm">

+ 2 - 0
BeefySysLib/BeefySysLib_static.vcxproj

@@ -199,6 +199,7 @@
     <ClCompile Include="gfx\Shader.cpp" />
     <ClCompile Include="gfx\Texture.cpp" />
     <ClCompile Include="img\BFIData.cpp" />
+    <ClCompile Include="img\BMPData.cpp" />
     <ClCompile Include="img\ImageAdjustments.cpp" />
     <ClCompile Include="img\ImageData.cpp" />
     <ClCompile Include="img\ImageUtils.cpp" />
@@ -908,6 +909,7 @@
     <ClInclude Include="gfx\Shader.h" />
     <ClInclude Include="gfx\Texture.h" />
     <ClInclude Include="img\BFIData.h" />
+    <ClInclude Include="img\BMPData.h" />
     <ClInclude Include="img\ImageAdjustments.h" />
     <ClInclude Include="img\ImageData.h" />
     <ClInclude Include="img\ImageUtils.h" />

+ 6 - 0
BeefySysLib/BeefySysLib_static.vcxproj.filters

@@ -590,6 +590,9 @@
     <ClCompile Include="third_party\putty\wildcard.c">
       <Filter>src\third_party\putty</Filter>
     </ClCompile>
+    <ClCompile Include="img\BMPData.cpp">
+      <Filter>src\img</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="Common.h">
@@ -907,6 +910,9 @@
     <ClInclude Include="third_party\putty\wildcard.h">
       <Filter>src\third_party\putty</Filter>
     </ClInclude>
+    <ClInclude Include="img\BMPData.h">
+      <Filter>src\img</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <CustomBuild Include="third_party\libffi\i686-pc-cygwin\src\x86\win32.asm">

+ 7 - 0
BeefySysLib/gfx/RenderDevice.cpp

@@ -6,6 +6,7 @@
 #include "img/TGAData.h"
 #include "img/PNGData.h"
 #include "img/PVRData.h"
+#include "img/BMPData.h"
 #include "img/BFIData.h"
 #include "img/JPEGData.h"
 #include "util/PerfTimer.h"
@@ -142,6 +143,12 @@ Texture* RenderDevice::LoadTexture(const StringImpl& fileName, int flags)
 		imageData = new JPEGData();
 	else if (ext == ".pvr")
 		imageData = new PVRData();
+	else if (ext == ".bmp")
+	{
+		BMPData* bmpData = new BMPData();
+		bmpData->mHasTransFollowing = (flags & TextureFlag_HasTransFollowing) == 0;;
+		imageData = bmpData;
+	}
 	else
 	{
 		return NULL; // Unknown format

+ 1 - 0
BeefySysLib/gfx/RenderDevice.h

@@ -156,6 +156,7 @@ enum TextureFlag : int8
 	TextureFlag_Additive = 1,
 	TextureFlag_NoPremult = 2,
 	TextureFlag_AllowRead = 4,
+	TextureFlag_HasTransFollowing = 8
 };
 
 struct VertexDefData

+ 531 - 0
BeefySysLib/img/BMPData.cpp

@@ -0,0 +1,531 @@
+#include "BMPData.h"
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+USING_NS_BF;
+
+#pragma warning(disable:4996)
+
+int	BMPData::Read(void* ptr, int elemSize, int elemCount)
+{	
+	int maxReadCount = (mSrcDataLen - mReadPos) / elemSize;
+	if (elemCount > maxReadCount)
+		elemCount = maxReadCount;
+	memcpy(ptr, mSrcData + mReadPos, elemCount * elemSize);
+	mReadPos += elemCount * elemSize;
+	return elemCount;
+}
+
+unsigned char BMPData::ReadC()
+{
+	if (mReadPos >= mSrcDataLen)
+		return 0;
+	return mSrcData[mReadPos++];
+}
+
+BMPData::BMPData()
+{
+	mHasTransFollowing = false;
+	mReadPos = 0;
+}
+
+#define BITMAP_MAGIC_NUMBER 19778
+
+typedef struct bmp_file_header_s bmp_file_header_t;
+struct bmp_file_header_s
+{
+	/*	int16_t magic_number; */ /* because of padding, we don't put it into the struct */
+	int32_t size;
+	int32_t app_id;
+	int32_t offset;
+};
+
+typedef struct bmp_bitmap_info_header_s bmp_bitmap_info_header_t;
+struct bmp_bitmap_info_header_s
+{
+	int32_t header_size;
+	int32_t width;
+	int32_t height;
+	int16_t num_planes;
+	int16_t bpp;
+	int32_t compression;
+	int32_t image_size;
+	int32_t horizontal_resolution;
+	int32_t vertical_resolution;
+	int32_t colors_used;
+	int32_t colors_important;
+};
+
+//typedef struct bmp_palette_element_s bmp_palette_element_t;
+
+
+bool BMPData::ReadPixelsRLE8(bmp_palette_element_t* palette)
+{
+	unsigned char byte, index, i, * p;
+	unsigned char x_ofs, y_ofs;
+	char keepreading = 1;
+	int current_line = 0;
+	unsigned char* dest = (unsigned char*)mBits;
+
+	p = dest;
+	while (keepreading) {	/* in each loop, we read a pair of bytes */
+		byte = ReadC();	/* read the first byte */
+		if (byte) {
+			index = ReadC(); /* read the second byte */
+			for (i = 0; i < byte; i++) {
+				*p = palette[index].red; p++;	/* unindex pixels on the fly */
+				*p = palette[index].green; p++;
+				*p = palette[index].blue; p++;
+				*p = 0xFF; p++;					/* add alpha */
+			}
+		}
+		else {
+			byte = ReadC();	/* read the second byte */
+			switch (byte)
+			{
+			case 0: /* skip the end of the current line and go to the next line */
+				current_line++;
+				p = dest + current_line * mWidth * 4;
+				break;
+			case 1: /* stop reading */
+				keepreading = 0;
+				break;
+			case 2: /* skip y_ofs lines and x_ofs columns. This means that the ignored pixels will be
+					filled with black (Or maybe i didn't understand the spec ?). This has already be done :
+					before starting to load pixel data, we filled all the dest[] array with 0x00 by a memset() */
+				x_ofs = ReadC();
+				y_ofs = ReadC();
+				current_line += y_ofs;
+				p += y_ofs * mWidth * 4 + x_ofs * 4;
+				break;
+			default: /* get the next n pixels, where n = byte */
+				for (i = 0; i < byte; i++) {
+					index = ReadC();
+					*p = palette[index].red; p++;	/* unindex pixels on the fly */
+					*p = palette[index].green; p++;
+					*p = palette[index].blue; p++;
+					*p = 0xFF; p++;					/* add alpha */
+				}
+				/* if n is not a multiple of 2, then skip one byte in the file, to respect int16_t alignment */
+				if (byte % 2) mReadPos++;
+				break;
+			}
+		}
+		/* the place to which p is pointing is guided by the content of the file. A corrupted file could make p point to a wrong localization.
+		   Hence, we must check that we don't point outside of the dest[] array. This may prevent an error of segmentation */
+		if (p >= dest + mWidth * mHeight * 4) keepreading = 0;
+	}
+
+	return true;
+}
+
+bool BMPData::ReadPixelsRLE4(bmp_palette_element_t* palette)
+{
+	unsigned char byte1, byte2, index1, index2, * p;
+	unsigned char x_ofs, y_ofs;
+	unsigned char bitmask = 0x0F; /* bit mask : 00001111 */
+	char keepreading = 1;
+	int current_line = 0;
+	int i;
+
+	unsigned char* dest = (unsigned char*)mBits;
+
+	p = dest;
+	while (keepreading) {
+		byte1 = ReadC();
+		if (byte1) {	/* encoded mode */
+			byte2 = ReadC();
+			index1 = byte2 >> 4;		/* get the first 4 bits of byte2 */
+			index2 = byte2 & bitmask;	/* get the next 4 bits of byte2 */
+			for (i = 0; i < (byte1 / 2); i++) {
+				*p = palette[index1].red; p++;
+				*p = palette[index1].green; p++;
+				*p = palette[index1].blue; p++;
+				*p = 0x0F; p++;
+				*p = palette[index2].red; p++;
+				*p = palette[index2].green; p++;
+				*p = palette[index2].blue; p++;
+				*p = 0x0F; p++;
+			}
+			if (byte1 % 2) {
+				*p = palette[index1].red; p++;
+				*p = palette[index1].green; p++;
+				*p = palette[index1].blue; p++;
+				*p = 0x0F; p++;
+			}
+		}
+		else {	/* absolute mode */
+			byte2 = ReadC();
+			switch (byte2)
+			{
+			case 0: /* skip the end of the current line and go to the next line */
+				current_line++;
+				p = dest + current_line * mWidth * 4;
+				break;
+			case 1: /* stop reading */
+				keepreading = 0;
+				break;
+			case 2: /* skip y_ofs lines and x_ofs column */
+				x_ofs = ReadC();
+				y_ofs = ReadC();
+				current_line += y_ofs;
+				p += y_ofs * mWidth * 4 + x_ofs * 4;
+				break;
+			default: /* get the next n pixels, where n = byte2 */
+				for (i = 0; i < (byte2 / 2); i++) {
+					byte1 = ReadC();
+					index1 = byte1 >> 4;
+					*p = palette[index1].red; p++;
+					*p = palette[index1].green; p++;
+					*p = palette[index1].blue; p++;
+					*p = 0x0F; p++;
+					index2 = byte1 & bitmask;
+					*p = palette[index2].red; p++;
+					*p = palette[index2].green; p++;
+					*p = palette[index2].blue; p++;
+					*p = 0x0F; p++;
+				}
+				if (byte2 % 2) {
+					byte1 = ReadC();
+					index1 = byte1 >> 4;
+					*p = palette[index1].red; p++;
+					*p = palette[index1].green; p++;
+					*p = palette[index1].blue; p++;
+					*p = 0x0F; p++;
+				}
+				if (((byte2 + 1) / 2) % 2) /* int16_t alignment */
+					mReadPos += 1;					
+				break;
+			}
+		}
+		/* the place to which p is pointing is guided by the content of the file. A corrupted file could make p point to a wrong localization.
+		   Hence, we must check that we don't point outside of the dest[] array. This may prevent an error of segmentation */
+		if (p >= dest + mWidth * mHeight * 4) keepreading = 0;
+	}
+
+	return true;
+}
+
+bool BMPData::ReadPixels32()
+{
+	int i, j;
+	unsigned char px[4], * p;
+
+	unsigned char* dest = (unsigned char*)mBits;
+	
+	for (i = 0; i < mHeight; i++) 
+	{
+		p = dest + (mHeight - i - 1) * mWidth * 4;
+		for (j = 0; j < mWidth; j++) 
+		{
+			Read(px, 4, 1); /* convert BGRX to RGBA */ 
+			*p = px[2]; p++; // R
+			*p = px[1]; p++;
+			*p = px[0]; p++;
+			*p = px[3]; p++;
+		}
+	}
+
+	return true;
+}
+
+bool BMPData::ReadPixels24()
+{
+	int i, j;
+	unsigned char px[3], * p;
+
+	unsigned char* dest = (unsigned char*)mBits;
+	
+	for (i = 0; i < mHeight; i++) 
+	{
+		p = dest + (mHeight - i - 1) * mWidth * 4;
+		for (j = 0; j < mWidth; j++) 
+		{
+			Read(px, 3, 1);
+			*p = px[2]; p++;	/* convert BGR to RGBA */
+			*p = px[1]; p++;
+			*p = px[0]; p++;
+			*p = 0xFF;  p++;	/* add alpha component */
+		}
+		if (mWidth * 3 % 4 != 0)
+			mReadPos += 4 - (mWidth * 3 % 4); /* if the width is not a multiple of 4, skip the end of the line */
+	}
+
+	return true;
+}
+
+/* Expected format : XBGR   0 11111 11111 11111 */
+bool BMPData::ReadPixels16()
+{
+	uint16_t pxl, r, g, b;
+	uint16_t bitmask = 0x1F;
+	unsigned char* p;
+	int i, j;
+
+	unsigned char* dest = (unsigned char*)mBits;
+
+	p = dest;
+	for (i = 0; i < mHeight; i++) 
+	{
+		for (j = 0; j < mWidth; j++) 
+		{
+			Read(&pxl, 2, 1);
+			b = (pxl >> 10) & bitmask;
+			g = (pxl >> 5) & bitmask;
+			r = pxl & bitmask;
+			*p = r * 8; p++; /* fix me */
+			*p = g * 8; p++;
+			*p = b * 8; p++;
+			*p = 0xFF; p++;
+		}
+		if ((2 * mWidth) % 4 != 0)
+			mReadPos += 4 - ((2 * mWidth) % 4);
+	}
+
+	return true;
+}
+
+bool BMPData::ReadPixels8(bmp_palette_element_t* palette)
+{
+	int i, j;
+	unsigned char px, * p;
+
+	unsigned char* dest = (unsigned char*)mBits;
+
+	p = dest;
+	for (i = 0; i < mHeight; i++) {
+		for (j = 0; j < mWidth; j++) {
+			Read(&px, 1, 1);
+			*p = palette[px].red; p++;
+			*p = palette[px].green; p++;
+			*p = palette[px].blue; p++;
+			*p = 0xFF; p++;
+		}
+		if (mWidth % 4 != 0)
+			mReadPos += 4 - (mWidth % 4);
+	}
+
+	return true;
+}
+
+bool BMPData::ReadPixels4(bmp_palette_element_t* palette)
+{
+	int size = (mWidth + 1) / 2;			/* byte alignment */
+	unsigned char* row_stride = new unsigned char[size];	/* not C90 but convenient here */
+	defer({ delete row_stride; });
+
+	unsigned char index, byte, * p;
+	unsigned char bitmask = 0x0F;	/* bit mask : 00001111 */
+
+	unsigned char* dest = (unsigned char*)mBits;
+
+	p = dest;
+	for (int i = 0; i < mHeight; i++) 
+	{
+		Read(row_stride, size, 1);
+		for (int j = 0; j < mWidth; j++) 
+		{
+			byte = row_stride[j / 2];
+			index = (j % 2) ? bitmask & byte : byte >> 4;
+			*p = palette[index].red; p++;
+			*p = palette[index].green; p++;
+			*p = palette[index].blue; p++;
+			*p = 0xFF; p++;
+		}
+		if (size % 4 != 0)
+			mReadPos += 4 - (size % 4);
+	}
+
+	return true;
+}
+
+bool BMPData::ReadPixels1(bmp_palette_element_t* palette)
+{
+	int size = (mWidth + 7) / 8;			/* byte alignment */
+	unsigned char* row_stride = new unsigned char[size];	/* not C90 but convenient here */
+	defer({ delete row_stride; });
+
+	unsigned char index, byte, * p;
+	unsigned char bitmask = 0x01;	/* bit mask : 00000001 */
+	int bit;
+
+	unsigned char* dest = (unsigned char*)mBits;
+
+	p = dest;
+	for (int i = 0; i < mHeight; i++) {
+		Read(row_stride, size, 1);
+		for (int j = 0; j < mWidth; j++) {
+			bit = (j % 8) + 1;
+			byte = row_stride[j / 8];
+			index = byte >> (8 - bit);
+			index &= bitmask;
+			*p = palette[index].red; p++;
+			*p = palette[index].green; p++;
+			*p = palette[index].blue; p++;
+			*p = 0xFF; p++;
+		}
+		if (size % 4 != 0)
+			mReadPos += 4 - (size % 4);
+	}
+
+	return true;
+}
+
+bool BMPData::ReadData()
+{	
+	int16_t magic_number;
+	bmp_file_header_t file_header;
+	bmp_bitmap_info_header_t info_header;
+	bmp_palette_element_t* palette = NULL;	
+	
+	Read(&magic_number, 2, 1);
+	if (magic_number == BITMAP_MAGIC_NUMBER) 
+	{		
+		Read((void*)&file_header, 12, 1);
+		Read((void*)&info_header, 40, 1);
+		mReadPos = file_header.offset;
+	}	
+	else
+	{
+		mReadPos = 0;
+		Read((void*)&info_header, 40, 1);
+	}
+	
+	/* info_header sanity checks */
+	/* accepted headers : bitmapinfoheader, bitmapv4header, bitmapv5header */
+	if (!(info_header.header_size == 40 || info_header.header_size == 108 || info_header.header_size == 124)) {		
+		return false;
+	}
+	if (info_header.num_planes != 1) {		
+		return false;
+	}
+	if (info_header.compression == 4 || info_header.compression == 5) {		
+		return false;
+	}
+	if (info_header.height < 0) {
+		return false;
+	}
+
+	/* load palette, if present */
+	if (info_header.bpp <= 8) {
+		mReadPos = 14 + info_header.header_size;
+		if ((info_header.bpp == 1) && (info_header.colors_used == 0)) info_header.colors_used = 2;
+		if ((info_header.bpp == 4) && (info_header.colors_used == 0)) info_header.colors_used = 16;
+		if ((info_header.bpp == 8) && (info_header.colors_used == 0)) info_header.colors_used = 256;
+		palette = (bmp_palette_element_t*)malloc(info_header.colors_used * sizeof(bmp_palette_element_t));
+		if (!palette) {			
+			return false;
+		}
+		else
+			Read((void*)palette, sizeof(bmp_palette_element_t), info_header.colors_used);
+	}
+
+	/* memory allocation */
+	//buf = malloc(info_header.width * info_header.height * 4);
+	mWidth = info_header.width;
+	mHeight = info_header.height;
+	mBits = new uint32[mWidth * mHeight];
+	
+	memset(mBits, 0x00, info_header.width * info_header.height * 4);
+
+	/* load image data */	
+	switch (info_header.bpp)
+	{
+	case 32:
+		ReadPixels32();
+		break;
+	case 24:
+		ReadPixels24();
+		break;
+	case 16:
+		ReadPixels16();
+		break;
+	case 8:
+		if (info_header.compression == 1)
+			ReadPixelsRLE8(palette);
+		else
+			ReadPixels8(palette);
+		break;
+	case 4:
+		if (info_header.compression == 2)
+			ReadPixelsRLE4(palette);
+		else
+			ReadPixels4(palette);
+		break;
+	case 1:
+		ReadPixels1(palette);
+		break;
+	}
+
+	if (info_header.colors_used) free(palette);	
+
+	if (mHasTransFollowing)
+	{
+		mHeight /= 2;
+		auto newBits = new uint32[mWidth * mHeight];
+		memcpy(newBits, mBits + mHeight * mWidth, mWidth * mHeight * 4);
+		delete mBits;
+		mBits = newBits;
+	}
+
+	return true;
+}
+
+bool BMPData::WriteToFile(const StringImpl& path)
+{
+	FILE* file;
+	int16_t magic_number;
+	bmp_file_header_t file_header;
+	bmp_bitmap_info_header_t info_header;
+	unsigned char sample[3], * p;
+	int i, j;
+
+	file = fopen(path.c_str(), "wb");
+	if (!file) 
+	{		
+		return false;
+	}
+
+	magic_number = BITMAP_MAGIC_NUMBER;
+
+	file_header.size = mWidth * mHeight * 4 + 54;
+	file_header.app_id = 0;
+	file_header.offset = 54;
+
+	info_header.header_size = 40;
+	info_header.width = mWidth;
+	info_header.height = mHeight;
+	info_header.num_planes = 1;
+	info_header.bpp = 24;
+	info_header.compression = 0;
+	info_header.image_size = mWidth * mHeight * 4;
+	info_header.horizontal_resolution = 0;
+	info_header.vertical_resolution = 0;
+	info_header.colors_used = 0;
+	info_header.colors_important = 0;
+
+	fwrite(&magic_number, sizeof(magic_number), 1, file);
+	fwrite(&file_header, sizeof(bmp_file_header_t), 1, file);
+	fwrite(&info_header, sizeof(bmp_bitmap_info_header_t), 1, file);
+
+	p = (unsigned char*)mBits;
+	for (i = 0; i < mHeight; i++) 
+	{
+		for (j = 0; j < mWidth; j++) 
+		{
+			/* convert RGBA to BGR */
+			sample[2] = *p; p++;
+			sample[1] = *p; p++;
+			sample[0] = *p; p++;
+			p++;
+
+			fwrite(sample, 3, 1, file);
+		}
+	}
+
+	fclose(file);
+
+	return 0;
+}

+ 41 - 0
BeefySysLib/img/BMPData.h

@@ -0,0 +1,41 @@
+#pragma once
+
+#include "ImageData.h"
+
+NS_BF_BEGIN;
+
+struct bmp_palette_element_s
+{
+	unsigned char blue;
+	unsigned char green;
+	unsigned char red;
+	unsigned char reserved;	/* alpha ? */
+};
+typedef struct bmp_palette_element_s bmp_palette_element_t;
+
+class BMPData : public ImageData
+{
+public:
+	int						mReadPos;
+	bool					mHasTransFollowing;
+
+	int						Read(void* ptr, int elemSize, int elemCount);
+	unsigned char			ReadC();
+
+	bool					ReadPixelsRLE8(bmp_palette_element_t* palette);
+	bool					ReadPixelsRLE4(bmp_palette_element_t* palette);
+	bool					ReadPixels32();
+	bool					ReadPixels24();
+	bool					ReadPixels16();
+	bool					ReadPixels8(bmp_palette_element_t* palette);
+	bool					ReadPixels4(bmp_palette_element_t* palette);
+	bool					ReadPixels1(bmp_palette_element_t* palette);
+
+public:
+	BMPData();
+
+	bool					ReadData();
+	bool					WriteToFile(const StringImpl& path);
+};
+
+NS_BF_END;

+ 3 - 0
BeefySysLib/platform/win/DXRenderDevice.cpp

@@ -692,6 +692,9 @@ void DXTexture::SetBits(int destX, int destY, int destWidth, int destHeight, int
 
 void DXTexture::GetBits(int srcX, int srcY, int srcWidth, int srcHeight, int destPitch, uint32* bits)
 {
+	if ((srcWidth <= 0) || (srcHeight <= 0))
+		return;
+
 	D3D11_TEXTURE2D_DESC texDesc;
 	texDesc.ArraySize = 1;
 	texDesc.BindFlags = 0;

+ 68 - 4
IDE/src/ui/StartupPanel.bf

@@ -1,3 +1,5 @@
+#pragma warning disable 168
+
 using System;
 using System.IO;
 using System.Collections;
@@ -9,6 +11,7 @@ using IDE.util;
 using Beefy.events;
 using Beefy.theme;
 using System.Diagnostics;
+using Beefy.utils;
 
 namespace IDE.ui
 {
@@ -17,6 +20,7 @@ namespace IDE.ui
 		class RecentWorkspacesScrollWidget : ScrollableWidget
 		{
 			public Font mTitleFont;
+			public bool mHasIcons;
 
 			public this()
 			{
@@ -31,7 +35,7 @@ namespace IDE.ui
 
 			public override void Resize(float x, float y, float width, float height)
 			{
-				const float MARGIN = 3;
+				const float MARGIN = 0;
 				float currentY = 0;
 
 				float fillWidth = width - (mVertScrollbar?.Width).GetValueOrDefault();
@@ -40,7 +44,7 @@ namespace IDE.ui
 				{
 					for (let widget in mScrollContent.mChildWidgets)
 					{
-						widget.Resize(0, currentY, fillWidth, GS!(30));
+						widget.Resize(0, currentY, fillWidth, GS!(33));
 						currentY += widget.Height + MARGIN;
 					}
 				}
@@ -60,7 +64,7 @@ namespace IDE.ui
 				g.SetFont(mTitleFont);
 
 				using (g.PushColor(gApp.mSettings.mUISettings.mColors.mText))
-					g.DrawString("Recent Workspaces", 0, GS!(-30), .Centered, mWidth, .Ellipsis);
+					g.DrawString("Recent Workspaces", GS!(2), GS!(-30), .Centered, mWidth - GS!(4), .Ellipsis);
 
 				base.Draw(g);
 			}
@@ -70,6 +74,8 @@ namespace IDE.ui
 		{
 			public static Font s_Font;
 			append String mPath;
+			public bool mTriedLoadIcon;
+			public Image mIcon ~ delete _;
 
 			public bool mPinned;
 			public RecentWorkspacesScrollWidget mRecentsParent;
@@ -91,9 +97,67 @@ namespace IDE.ui
 					
 				}
 
+				if (!mTriedLoadIcon)
+				{
+					mTriedLoadIcon = true;
+
+					StructuredData sd = scope .();
+					if (sd.Load(scope $"{mPath}/BeefProj.toml") case .Ok)
+					{
+						using (sd.Open("Platform"))
+						{
+							using (sd.Open("Windows"))
+							{
+								var iconFileName = sd.GetString("IconFile", .. scope .());
+								iconFileName.Replace("$(ProjectDir)", mPath);
+								iconFileName.Replace("$(WorkspaceDir)", mPath);
+								var iconFilePath = IO.Path.GetAbsolutePath(iconFileName, mPath, .. scope .());
+								
+								if (File.Exists(iconFilePath))
+								{
+									int wantSize = 32;
+									if (var image = ResourceGen.LoadIcon(iconFilePath, wantSize))
+									{
+										if ((image.mWidth != wantSize) || (image.mHeight != wantSize))
+										{
+											image.Scale(wantSize / Math.Max(image.mWidth, image.mHeight));
+										}
+										mIcon = image;
+										mRecentsParent.mHasIcons = true;
+									}
+								}
+							}
+						}
+					}
+				}
+
 				g.SetFont(s_Font);
 				using (g.PushColor(gApp.mSettings.mUISettings.mColors.mText))
-					g.DrawString(mPath, 10, 0, .Left, mWidth - 10);
+				{
+					String drawStr = scope String();
+					int lastSlash = Math.Max(mPath.LastIndexOf('\\'), mPath.LastIndexOf('/'));
+					if (lastSlash != -1)
+					{
+						drawStr.Append(Font.EncodeColor(0x80FFFFFF));
+						drawStr.Append(mPath.Substring(0, lastSlash + 1));
+						drawStr.Append(Font.EncodePopColor());
+						drawStr.Append(mPath.Substring(lastSlash + 1));
+					}
+					else
+					{
+						drawStr.Append(mPath);
+					}
+
+					float drawX = GS!(50);
+					g.DrawString(drawStr, drawX, GS!(3), .Left, mWidth - drawX - GS!(2), .Ellipsis);
+				}
+				if (mIcon != null)
+					g.DrawCentered(mIcon, GS!(24), mHeight / 2);
+				else
+				{
+					using (g.PushColor(0x80FFFFFF))
+						g.DrawCentered(DarkTheme.sDarkTheme.GetImage(.Workspace), GS!(24), mHeight / 2);
+				}
 			}
 
 			public override void MouseEnter()

+ 58 - 0
IDE/src/util/ResourceGen.bf

@@ -1,8 +1,12 @@
+#pragma warning disable 168
+
 using System;
 using System.IO;
 using System.Collections;
 using System.Text;
 using System.Diagnostics;
+using Beefy.gfx;
+
 namespace IDE.util
 {
 	class ResourceGen
@@ -62,6 +66,60 @@ namespace IDE.util
 
 		FileStream mOutStream = new FileStream() ~ delete _;
 
+		public static Result<Image> LoadIcon(String iconFile, int wantSize = -1)
+		{
+			FileStream stream = scope FileStream();
+			if (stream.Open(iconFile, .Read) case .Err)
+			{
+				return .Err;
+			}
+
+			let iconDir = Try!(stream.Read<IconDir>());
+			if ((iconDir.mReserved != 0) || (iconDir.mType != 1) || (iconDir.mCount > 0x100))
+			{
+				return .Err;
+			}
+
+			var entries = scope List<IconDirectoryEntry>();
+
+			for (int idx < iconDir.mCount)
+			{
+				entries.Add(Try!(stream.Read<IconDirectoryEntry>()));
+			}
+
+			int bestIdx = iconDir.mCount - 1;
+			for (int idx < iconDir.mCount)
+			{
+				let iconEntry = ref entries[idx];
+				if (iconEntry.mWidth >= wantSize)
+				{
+					bestIdx = idx;
+					break;
+				}
+			}
+
+			let iconEntry = ref entries[bestIdx];
+
+			Try!(stream.Seek(iconEntry.mImageOffset));
+
+			if (iconEntry.mBytesInRes > 1024*1024)
+			{
+				return .Err;
+			}
+
+			uint8* data = new:ScopedAlloc! uint8[iconEntry.mBytesInRes]*;
+
+			Try!(stream.TryRead(.(data, iconEntry.mBytesInRes)));
+
+			String bmpPath = scope $"@{(int)(void*)data:X}:{iconEntry.mBytesInRes}.bmp";
+
+			var image = Image.LoadFromFile(bmpPath);
+			if (image != null)
+				return image;
+
+			return .Err;
+		}
+
 		public Result<void> AddIcon(String iconFile)
 		{
 			if (!iconFile.EndsWith(".ico", .OrdinalIgnoreCase))