# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: colin@gibibit.com-20080812025506-uxychfeydaod9wap # target_branch: ../trunk-clean # testament_sha1: 9db9fd86bc3b44cbe9cf194d4ed98d1af80f929a # timestamp: 2008-08-11 19:59:56 -0700 # source_branch: http://grub.gibibit.com/bzr/trunk-clean # base_revision_id: colin@gibibit.com-20080811144250-qvttic16dey3cnvb # # Begin patch === modified file '.bzrignore' --- .bzrignore 2008-07-19 21:43:42 +0000 +++ .bzrignore 2008-07-19 21:44:04 +0000 @@ -7,3 +7,5 @@ ./conf/sparc64-ieee1275.mk ./conf/i386-coreboot.mk ./build* +./util/fonttool/build +./util/fonttool/fonttool.iws === modified file 'ChangeLog' --- ChangeLog 2008-08-11 09:33:14 +0000 +++ ChangeLog 2008-08-11 14:43:03 +0000 @@ -405,7 +405,6 @@ (memdisk_mod_SOURCES, memdisk_mod_CFLAGS) (memdisk_mod_LDFLAGS): New variables. * conf/i386-coreboot.rmk: Likewise. - * conf/i386-ieee1275.rmk: Likewise. 2008-08-02 Robert Millan @@ -538,6 +537,72 @@ * normal/main.c (get_line): Fix buffer overflow bug. +2008-07-28 Colin D Bennett + + High resolution timer support. Implemented for x86 CPUs using TSC. + Extracted generic grub_millisleep() so it's linked in only as needed. + This requires a Pentium compatible CPU; if the RDTSC instruction is + not supported, then it falls back on the generic grub_get_time_ms() + implementation that uses the machine's RTC. + + * conf/i386-efi.rmk: Added new source files to kernel_elf_SOURCES. + + * conf/i386-pc.rmk: Likewise. + + * conf/sparc64-ieee1275.rmk: Likewise. + + * conf/powerpc-ieee1275.rmk: Likewise. + + + * kern/generic/rtc_get_time_ms.c: New file. + + * kern/generic/millisleep.c: New file. + + * kern/misc.c (grub_millisleep_generic): Removed. + + * commands/sleep.c (grub_interruptible_millisleep): Uses + grub_get_time_ms() instead of grub_get_rtc() to stay in sync with + grub_millisleep() from kern/generic/millisleep.c. + + * include/grub/i386/tsc.h (grub_get_tsc): New file. New inline + function. + (grub_cpu_is_cpuid_supported): New inline function. + (grub_cpu_is_tsc_supported): New inline function. + (grub_tsc_init): New function prototype. + (grub_tsc_get_time_ms): New function prototype. + + * kern/i386/tsc.c (grub_get_time_ms): New file. New function. + (calibrate_tsc): New static function. + (grub_tsc_init): New function. + + * include/grub/time.h (grub_millisleep_generic): Removed. + (grub_get_time_ms): New function. + (grub_install_get_time_ms): New function. + + * kern/time.c (grub_get_time_ms): New function. + (grub_install_get_time_ms): New function. + + * kern/i386/efi/init.c (grub_millisleep): Removed. + (grub_machine_init): Call grub_tsc_init. + + * kern/i386/linuxbios/init.c (grub_machine_init): Install the RTC + get_time_ms() implementation. + + * kern/sparc64/ieee1275/init.c (grub_millisleep): Removed. + (ieee1275_get_time_ms): New function. + (grub_machine_init): Install get_time_ms() implementation. + + * kern/i386/pc/init.c (grub_machine_init): Call grub_tsc_init(). + (grub_millisleep): Removed. + + * kern/ieee1275/init.c (grub_millisleep): Removed. + (grub_machine_init): Install ieee1275_get_time_ms() implementation. + (ieee1275_get_time_ms): New static function. + (grub_get_rtc): Now calls ieee1275_get_time_ms(), which does the real + work. + + * conf/i386-ieee1275.rmk: Likewise. + 2008-07-28 Robert Millan * partmap/apple.c (GRUB_APPLE_HEADER_MAGIC): New macro. === modified file 'Makefile.in' --- Makefile.in 2008-08-09 11:30:26 +0000 +++ Makefile.in 2008-08-11 14:43:03 +0000 @@ -90,6 +90,39 @@ YACC = @YACC@ UNIFONT_HEX = @UNIFONT_HEX@ +### Pretty output control ### +# Set up compiler and linker commands that either is quiet (does not print +# the command line being executed) or verbose (print the command line). +_CC := $(CC) +_TARGET_CC := $(TARGET_CC) +_STRIP := $(STRIP) +_GENMODSRC := sh $(srcdir)/genmodsrc.sh +ifeq ($(V),1) + override V_PREFIX := + override CC = $(_CC) + override TARGET_CC = $(_CC) + override STRIP = $(_STRIP) + override GENMODSRC = $(_GENMODSRC) + override INFO_GENCMDLIST = + override INFO_GENFSLIST = + override INFO_GENPARTMAPLIST = + override INFO_GEN_FINAL_COMMAND_LIST = + override INFO_GEN_FINAL_FS_LIST = + override INFO_GEN_FINAL_PARTMAP_LIST = +else + override V_PREFIX := @ + override CC = @echo "COMPILE $<"; $(_CC) + override TARGET_CC = @echo "COMPILE(TARGET) $<"; $(_TARGET_CC) + override STRIP = @echo "STRIP $@"; $(_STRIP) + override GENMODSRC = @echo "GENMODSRC $@"; $(_GENMODSRC) + override INFO_GENCMDLIST = @echo "GENCMDLIST $@" + override INFO_GENFSLIST = @echo "GENFSLIST $@" + override INFO_GENPARTMAPLIST = @echo "GENPARTMAPLIST $@" + override INFO_GEN_FINAL_COMMAND_LIST = @echo "GENCMDLIST[final] $@" + override INFO_GEN_FINAL_FS_LIST = @echo "GENFSLIST[final] $@" + override INFO_GEN_FINAL_PARTMAP_LIST = @echo "GENPARTMAPLIST[final] $@" +endif + # Options. enable_grub_emu = @enable_grub_emu@ enable_grub_fstest = @enable_grub_fstest@ @@ -138,13 +171,16 @@ || (rm -f $@; exit 1) command.lst: $(COMMANDFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_COMMAND_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ fs.lst: $(FSFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_FS_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ partmap.lst: $(PARTMAPFILES) - cat $^ /dev/null | sort > $@ + $(INFO_GEN_FINAL_PARTMAP_LIST) + $(V_PREFIX)cat $^ /dev/null | sort > $@ ifeq (, $(UNIFONT_HEX)) else === modified file 'commands/i386/pc/vbeinfo.c' --- commands/i386/pc/vbeinfo.c 2007-07-21 22:32:33 +0000 +++ commands/i386/pc/vbeinfo.c 2008-07-09 14:39:39 +0000 @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -48,12 +49,22 @@ grub_err_t err; char *modevar; - grub_printf ("List of compatible video modes:\n"); - err = grub_vbe_probe (&controller_info); if (err != GRUB_ERR_NONE) return err; + int row = 0; + + grub_printf ("VBE info: version: %d.%d OEM software rev: %d.%d\n", + controller_info.version >> 8, + controller_info.version & 0xFF, + controller_info.oem_software_rev >> 8, + controller_info.oem_software_rev & 0xFF); + row++; + grub_printf (" total memory: %d KiB\n", + (controller_info.total_memory << 16) / 1024); + row++; + /* Because the information on video modes is stored in a temporary place, it is better to copy it to somewhere safe. */ p = video_mode_list = real2pm (controller_info.video_mode_ptr); @@ -67,6 +78,10 @@ grub_memcpy (saved_video_mode_list, video_mode_list, video_mode_list_size); + grub_printf ("List of compatible video modes:\n"); + grub_printf ("Legend: P=Packed pixel, D=Direct color, " + "mask/pos=R/G/B/reserved\n"); + /* Walk through all video modes listed. */ for (p = saved_video_mode_list; *p != 0xFFFF; p++) { @@ -103,10 +118,10 @@ switch (mode_info_tmp.memory_model) { case 0x04: - memory_model = "Packed Pixel"; + memory_model = "Packed"; break; case 0x06: - memory_model = "Direct Color"; + memory_model = "Direct"; break; default: @@ -116,12 +131,31 @@ if (! memory_model) continue; - grub_printf ("0x%03x: %d x %d x %d bpp (%s)\n", - mode, + grub_printf ("0x%03x: %4dx%4dx%2d %s", + mode, mode_info_tmp.x_resolution, mode_info_tmp.y_resolution, mode_info_tmp.bits_per_pixel, - memory_model); + memory_model); + if (memory_model[0] == 'D') + grub_printf (" mask: %d/%d/%d/%d pos: %d/%d/%d/%d", + mode_info_tmp.red_mask_size, + mode_info_tmp.green_mask_size, + mode_info_tmp.blue_mask_size, + mode_info_tmp.rsvd_mask_size, + mode_info_tmp.red_field_position, + mode_info_tmp.green_field_position, + mode_info_tmp.blue_field_position, + mode_info_tmp.rsvd_field_position); + grub_printf ("\n"); + + if (++row >= 25) + { + grub_printf ("-- More --"); + grub_getkey (); + grub_printf ("\r \r"); + row = 0; + } } grub_free (saved_video_mode_list); === modified file 'commands/videotest.c' --- commands/videotest.c 2007-07-21 22:32:33 +0000 +++ commands/videotest.c 2008-08-11 15:02:14 +0000 @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -26,15 +27,38 @@ #include #include #include - -static grub_err_t -grub_cmd_videotest (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ +#include +#include +#include /* for grub_vbe_bios_set_display_start () test */ +/* Option array indices. */ +#define ARGINDEX_TEST_TIME 0 +#define ARGINDEX_DOUBLE_BUF 1 + +static const struct grub_arg_option arg_options[] = { + {"time", 't', 0, "Time to run each test, in seconds.", 0, ARG_TYPE_INT}, + {"dbuf", 'd', 0, "Use double buffered graphics.", 0, ARG_TYPE_NONE}, + {0, 0, 0, 0, 0, 0} +}; + +#define DEFAULT_TEST_TIME 5 + +/* Command options -- populated base on command line arguments. */ +struct videotest_options +{ + int test_time; + int double_buffering; +}; + + +static void +basic_video_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; if (grub_video_setup (1024, 768, GRUB_VIDEO_MODE_TYPE_INDEX_COLOR) != GRUB_ERR_NONE) - return grub_errno; + return; grub_getkey (); @@ -44,7 +68,8 @@ unsigned int width; unsigned int height; int i; - struct grub_font_glyph glyph; + grub_font_t font; + struct grub_font_glyph *glyph; struct grub_video_render_target *text_layer; grub_video_color_t palette[16]; @@ -65,8 +90,15 @@ color = grub_video_map_rgb (0, 255, 255); grub_video_fill_rect (color, 100, 100, 100, 100); - grub_font_get_glyph ('*', &glyph); - grub_video_blit_glyph (&glyph, color, 200 ,0); + font = grub_font_get ("Helvetica Bold 14"); + if (! font) + { + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + glyph = grub_font_get_glyph (font, '*'); + grub_video_blit_glyph (glyph, color, 200 ,0); grub_video_set_viewport (x + 150, y + 150, width - 150 * 2, height - 150 * 2); @@ -77,18 +109,18 @@ color = grub_video_map_rgb (255, 255, 255); - grub_font_get_glyph ('A', &glyph); - grub_video_blit_glyph (&glyph, color, 16, 16); - grub_font_get_glyph ('B', &glyph); - grub_video_blit_glyph (&glyph, color, 16 * 2, 16); + glyph = grub_font_get_glyph (font, 'A'); + grub_video_blit_glyph (glyph, color, 16, 16); + glyph = grub_font_get_glyph (font, 'B'); + grub_video_blit_glyph (glyph, color, 16 * 2, 16); - grub_font_get_glyph ('*', &glyph); + glyph = grub_font_get_glyph (font, '*'); for (i = 0; i < 16; i++) { color = grub_video_map_color (i); palette[i] = color; - grub_video_blit_glyph (&glyph, color, 16 + i * 16, 32); + grub_video_blit_glyph (glyph, color, 16 + i * 16, 32); } grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); @@ -101,30 +133,1076 @@ 0, 0, width, height); } + grub_video_swap_buffers (); + grub_getkey (); + + /* Test VBE set display start address. */ + /* This should scroll the screen, first vertically, + * then horizontally. The horizontal scrolling seems to + * only have a resolution of about 16 pixels on my VIA Mini-ITX. */ + int vbestatus; + int vbeok = 0; + int vbeerr = 0; + for (i = 0; i < 50; i++) + { + vbestatus = grub_vbe_bios_set_display_start (0, i); + if (vbestatus == GRUB_VBE_STATUS_OK) + vbeok++; + else + vbeerr++; + } + + + grub_getkey (); + + for (i = 0; i < 50; i++) + { + vbestatus = grub_vbe_bios_set_display_start (i, 50); + if (vbestatus == GRUB_VBE_STATUS_OK) + vbeok++; + else + vbeerr++; + } + grub_getkey (); grub_video_delete_render_target (text_layer); - - grub_video_restore (); - - for (i = 0; i < 16; i++) - grub_printf("color %d: %08x\n", i, palette[i]); - - grub_errno = GRUB_ERR_NONE; + grub_video_restore (); + + grub_printf ("VBE set_display_start status: %d\n", vbestatus); + grub_printf ("ok: %d\n", vbeok); + grub_printf ("errors: %d\n", vbeerr); + + grub_errno = GRUB_ERR_NONE; +} + + + +/** + * Simple opaque image blit test. + * Returns the error status in grub_errno. + */ +static void +bitmap_demo (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + const int bm1width = 500, bm1height = 400; + struct grub_video_bitmap *bitmap1; + + if (grub_video_bitmap_create (&bitmap1, bm1width, bm1height, + GRUB_VIDEO_BLIT_FORMAT_RGB_888) + != GRUB_ERR_NONE) + return; + + int offset = 0; + int x; + int y; + grub_uint8_t *data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bm1height; y++) + { + for (x = 0; x < bm1width; x++) + { + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + } + } + + /* Blit the entire bitmap in the center of the screen. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + (view.width - bm1width) / 2, + view.y + (view.height - bm1height) / 2, + 0, 0, bm1width, bm1height); + grub_video_swap_buffers (); + grub_getkey (); + + /* Blit more copies of the bitmap. */ + /* Upper left. */ + grub_video_blit_bitmap (bitmap1, GRUB_VIDEO_BLIT_REPLACE, + view.x, view.y, 0, 0, bm1width, bm1height); + /* Upper right. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + view.width - bm1width, + view.y, 0, 0, bm1width, bm1height); + /* Lower left. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x, + view.y + view.height - bm1height, + 0, 0, bm1width, bm1height); + /* Lower right. */ + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + view.width - bm1width, + view.y + view.height - bm1height, + 0, 0, bm1width, bm1height); + grub_video_swap_buffers (); + grub_getkey (); + + int clearbg = 0; /* Boolean flag: whether to fill background. */ + grub_video_color_t bgcolor = grub_video_map_rgb (16, 16, 96); + + /* Animate the image sliding in. End when a key is pressed. */ + int vscale = 1000; + int velocityx = -5000; + int velocityy = -8000; + int positionx = 100; + int positiony = 300; + + grub_uint32_t frame_count = 0; + grub_uint32_t start_time = grub_get_time_ms (); + + while (grub_checkkey () == -1) + { + /* If the time limit option is set, then check if it's exceeded. */ + if (vt_opts->test_time != 0 + && grub_get_time_ms () >= start_time + vt_opts->test_time * 1000) + break; + + int newx = positionx + velocityx / vscale; + int newy = positiony + velocityy / vscale; + + /* Check collision w/ left */ + if (newx < view.x && velocityx < 0) + { + velocityx = -velocityx; + newx = positionx + velocityx / vscale; + } + /* Check collision w/ right */ + if (newx + bm1width > view.x + view.width && velocityx > 0) + { + velocityx = -velocityx; + newx = positionx + velocityx / vscale; + } + /* Check collision w/ top */ + if (newy < 0 && velocityy < 0) + { + velocityy = -velocityy; + newy = positiony + velocityy / vscale; + } + /* Check collision w/ bottom */ + if (newy + bm1height > view.y + view.height && velocityy > 0) + { + velocityy = -velocityy; + newy = positiony + velocityy / vscale; + } + + positionx = newx; + positiony = newy; + + if (clearbg) + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, + view.height); + + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_REPLACE, + view.x + positionx, + view.y + positiony, 0, 0, bm1width, bm1height); + grub_video_swap_buffers (); + frame_count++; + + /* Acceleration due to gravity... + * note that the y coordinate is increasing downward. */ + velocityy += vscale / 7; + } + + /* Calculate average frame rate. */ + grub_uint32_t duration = grub_get_time_ms () - start_time; + grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration; + + /* Eat the keystroke. */ + if (grub_checkkey () != -1) + grub_getkey (); + + grub_video_bitmap_destroy (bitmap1); + grub_video_restore (); + + grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10); + + grub_errno = GRUB_ERR_NONE; +} + + + +/* Configuration settings for a benchmark run. */ +struct benchmark_config +{ + int width; + int height; + unsigned int mode_type; + int use_rgba_bitmaps; +}; + +/* Macro to make the benchmark_configs[] declaration more concise. */ +#define MODE_TYPE_RGB(bpp) (GRUB_VIDEO_MODE_TYPE_RGB \ + | ((bpp) << GRUB_VIDEO_MODE_TYPE_DEPTH_POS)) + +/* The video configurations to use for the benchmark. */ +static struct benchmark_config benchmark_configs[] = { + {320, 200, MODE_TYPE_RGB (0), 0}, + {640, 480, MODE_TYPE_RGB (0), 0}, + {1024, 768, MODE_TYPE_RGB (0), 0}, + {1024, 768, MODE_TYPE_RGB (0), 1}, +}; + +#define NUM_BENCHMARK_CONFIGS (sizeof(benchmark_configs) \ + / sizeof(benchmark_configs[0])) + +struct benchmark_result +{ + /* If set to 1, the test was able to run successfully; + * 0 means there was an error. */ + int test_passed; + + /* Bits per pixel for the video mode used. */ + int bpp; + + /* All fps are in fps * 10 to achieve one decimal place. */ + /* Set to 0 to indicate the test could not be run. */ + grub_int32_t fill_fps; + grub_int32_t blit_fps; + grub_int32_t blend_fps; +}; + +#define BENCHMARK_RESULT_FPS_SCALE 10 + +static void +move_rectangle_one_step (int *x, int *y, + int width, int height, + int *vx, int *vy, const grub_video_rect_t * bounds) +{ + int newx = *x + *vx; + int newy = *y + *vy; + + /* Check collision w/ left */ + if (newx < bounds->x && *vx < 0) + { + *vx = -*vx; + newx = *x + *vx; + } + /* Check collision w/ right */ + if (newx + width > bounds->x + bounds->width && *vx > 0) + { + *vx = -*vx; + newx = *x + *vx; + } + /* Check collision w/ top */ + if (newy < 0 && *vy < 0) + { + *vy = -*vy; + newy = *y + *vy; + } + /* Check collision w/ bottom */ + if (newy + height > bounds->y + bounds->height && *vy > 0) + { + *vy = -*vy; + newy = *y + *vy; + } + + *x = newx; + *y = newy; +} + +/** + * Run the benchmark test for a particular video mode, which is specified + * by ``*config``. The results of the test are stored in ``*result``. + */ +static void +do_benchmark (const struct benchmark_config *config, + struct benchmark_result *result, + struct videotest_options *vt_opts) +{ + struct grub_video_mode_info modeinfo; + + result->test_passed = 0; + result->fill_fps = 0; + result->blit_fps = 0; + result->blend_fps = 0; + result->bpp = 0; + + int mode_type = config->mode_type; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (config->width, config->height, + mode_type) != GRUB_ERR_NONE) + return; + + if (grub_video_get_info (&modeinfo) == GRUB_ERR_NONE) + result->bpp = modeinfo.bpp; + + /* Full screen bitmap blit test. */ + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + + /* For measuring timing. */ + grub_uint32_t frame_count; + grub_uint64_t start_time; + grub_uint64_t desired_stop_time; + grub_uint32_t duration; + + + /*** FILL TEST ***/ + + /* Alternates between 0 and 1 to change the color. */ + int color_flag = 0; + grub_video_color_t fillcolors[2]; + fillcolors[0] = grub_video_map_rgb (0, 0, 255); + fillcolors[1] = grub_video_map_rgb (255, 255, 0); + + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + grub_video_fill_rect (fillcolors[color_flag], + view.x, view.y, view.width, view.height); + grub_video_swap_buffers (); + color_flag ^= 1; + frame_count++; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was one. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->fill_fps = 0; + } + else + { + result->fill_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + + /*** BLIT TEST ***/ + + /* Generate two bitmaps, the same size as the screen. */ + const int bitmapwidth = view.width, bitmapheight = view.height; + struct grub_video_bitmap *bitmap1; + struct grub_video_bitmap *bitmap2; + enum grub_video_blit_format bitmap_format = config->use_rgba_bitmaps + ? GRUB_VIDEO_BLIT_FORMAT_RGBA_8888 : GRUB_VIDEO_BLIT_FORMAT_RGB_888; + + if (grub_video_bitmap_create (&bitmap1, bitmapwidth, bitmapheight, + bitmap_format) != GRUB_ERR_NONE) + return; + + if (grub_video_bitmap_create (&bitmap2, bitmapwidth, bitmapheight, + bitmap_format) != GRUB_ERR_NONE) + { + grub_video_bitmap_destroy (bitmap1); + return; + } + + int offset; + int x; + int y; + grub_uint8_t *data; + + offset = 0; + data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bitmapheight; y++) + { + for (x = 0; x < bitmapwidth; x++) + { + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + if (config->use_rgba_bitmaps) + data[offset++] = 255; + } + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap2); + for (y = 0; y < bitmapheight; y++) + { + for (x = 0; x < bitmapwidth; x++) + { + data[offset++] = x + y; /* red */ + data[offset++] = x * x + y * y; /* green */ + data[offset++] = x * x / 4 + y * y / 4; /* blue */ + if (config->use_rgba_bitmaps) + data[offset++] = 255; + } + } + + + /* Now do the blit test, alternating between the two bitmaps. */ + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + int cur = 0; /* Which bitmap to draw this frame. */ + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + struct grub_video_bitmap *current_bitmap = cur == 0 ? bitmap1 : bitmap2; + grub_video_blit_bitmap (current_bitmap, + GRUB_VIDEO_BLIT_REPLACE, + view.x, view.y, 0, 0, bitmapwidth, + bitmapheight); + grub_video_swap_buffers (); + frame_count++; + cur ^= 1; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was once. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->blit_fps = 0; + } + else + { + result->blit_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + grub_video_bitmap_destroy (bitmap2); + grub_video_bitmap_destroy (bitmap1); + + + + /*** BLEND TEST ***/ + + /* Generate two bitmaps, with alpha translucency. */ + const int bbw = view.width * 2 / 3; + const int bbh = view.height * 2 / 3; + + if (grub_video_bitmap_create (&bitmap1, bbw, bbh, + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) != + GRUB_ERR_NONE) + return; + + if (grub_video_bitmap_create (&bitmap2, bbw, bbh, + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) != + GRUB_ERR_NONE) + { + grub_video_bitmap_destroy (bitmap1); + return; + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap1); + for (y = 0; y < bbh; y++) + { + for (x = 0; x < bbw; x++) + { + /* Calculate a to be increasing away from the center. */ + int dx = 256 * (x - bbw / 2) / (bbw / 2); + int dy = 256 * (y - bbh / 2) / (bbh / 2); + int a = dx * dx + dy * dy; + /* range for a = 0 .. 2*(256^2) = 2*2^16 = 2^17 */ + a >>= 17 - 8; /* Make range 0..256. */ + if (a > 255) + a = 255; + + data[offset++] = x ^ y; /* red */ + data[offset++] = (x * 3) ^ (y * 3); /* green */ + data[offset++] = (x * 2) ^ (y * 2); /* blue */ + data[offset++] = 255 - a; + } + } + + offset = 0; + data = grub_video_bitmap_get_data (bitmap2); + for (y = 0; y < bbh; y++) + { + for (x = 0; x < bbw; x++) + { + data[offset++] = x + y; /* red */ + data[offset++] = x * x + y * y; /* green */ + data[offset++] = x * x / 4 + y * y / 4; /* blue */ + data[offset++] = 255; + } + } + + frame_count = 0; + start_time = grub_get_time_ms (); + desired_stop_time = start_time + vt_opts->test_time * 1000; + + + grub_video_color_t bgcolor = grub_video_map_rgb (80, 80, 80); + + /* Bitmap locations. */ + int b1x = 0; + int b1y = 0; + int b2x = view.width - bbw; + int b2y = 0; + /* Bitmap velocities. */ + int b1vx = 8; + int b1vy = 12; + int b2vx = -10; + int b2vy = 9; + + while (grub_get_time_ms () < desired_stop_time && grub_checkkey () == -1) + { + move_rectangle_one_step (&b1x, &b1y, bbw, bbh, &b1vx, &b1vy, &view); + move_rectangle_one_step (&b2x, &b2y, bbw, bbh, &b2vx, &b2vy, &view); + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height); + grub_video_blit_bitmap (bitmap2, + GRUB_VIDEO_BLIT_BLEND, + b2x, b2y, 0, 0, bbw, bbh); + grub_video_blit_bitmap (bitmap1, + GRUB_VIDEO_BLIT_BLEND, + b1x, b1y, 0, 0, bbw, bbh); + grub_video_swap_buffers (); + frame_count++; + cur ^= 1; + } + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was once. */ + + /* Calculate average frame rate. */ + duration = grub_get_time_ms () - start_time; + if (duration == 0) + { + result->blend_fps = 0; + } + else + { + result->blend_fps = + (BENCHMARK_RESULT_FPS_SCALE * 1000 + * frame_count / duration); + } + + grub_video_bitmap_destroy (bitmap2); + grub_video_bitmap_destroy (bitmap1); + + grub_video_restore (); + result->test_passed = 1; +} + +/** + * Run a benchmark test in a series of video modes. + * The results are reported in tabular form. This will be helpful to + * determine how effective various optimizations are. + */ +static void +benchmark_test (struct videotest_options *vt_opts) +{ + unsigned int i; + struct benchmark_result results[NUM_BENCHMARK_CONFIGS]; + + /* Set option default values. */ + if (vt_opts->test_time == 0) + vt_opts->test_time = DEFAULT_TEST_TIME; + + /* Run benchmarks. */ + for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++) + { + grub_error_push (); + do_benchmark (&benchmark_configs[i], &results[i], vt_opts); + } + + grub_print_error (); + + /* Display results. */ + grub_printf ("Benchmark results (in frames/s):\n"); + grub_printf ("(W=Width, H=Height, B=Bits per pixel,\n" + " T=Mode Type, A=Bitmap Alpha)\n"); + grub_printf (" W H B T A FILL BLIT BLEND\n"); + for (i = 0; i < NUM_BENCHMARK_CONFIGS; i++) + { + struct benchmark_config *c = &benchmark_configs[i]; + struct benchmark_result *r = &results[i]; + + if (r->test_passed) + { + grub_printf ("%4dx%4d %2d %d %d: %4d.%d %4d.%d %4d.%d\n", + c->width, c->height, r->bpp, + c->mode_type & 0x0F, + c->use_rgba_bitmaps, + r->fill_fps / BENCHMARK_RESULT_FPS_SCALE, + r->fill_fps % BENCHMARK_RESULT_FPS_SCALE, + r->blit_fps / BENCHMARK_RESULT_FPS_SCALE, + r->blit_fps % BENCHMARK_RESULT_FPS_SCALE, + r->blend_fps / BENCHMARK_RESULT_FPS_SCALE, + r->blend_fps % BENCHMARK_RESULT_FPS_SCALE); + } + else + { + grub_printf ("%4dx%4d %2d %d Not supported.\n", + c->width, c->height, + ((c->mode_type & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK) + >> GRUB_VIDEO_MODE_TYPE_DEPTH_POS), + c->mode_type & 0x0F); + } + } + + grub_errno = GRUB_ERR_NONE; +} + + +/** + * Test time functions. + */ +static void +clock_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + /* Draw a progress bar that animates in sync with time. */ + + grub_video_rect_t bar_frame; + bar_frame.width = view.width - 2 * view.width / 10; + bar_frame.height = view.height / 20; + bar_frame.x = view.x + view.width / 10; + bar_frame.y = view.y + view.height - bar_frame.height - view.height / 10; + + grub_video_color_t bgcolor = grub_video_map_rgb (50, 50, 50); + grub_video_color_t framecolor = grub_video_map_rgb (255, 255, 255); + grub_video_color_t barbgcolor = grub_video_map_rgb (0, 0, 128); + grub_video_color_t barcolor = grub_video_map_rgb (100, 100, 255); + + grub_uint32_t frame_count; + grub_uint64_t start_time; + grub_uint64_t barstart; + grub_uint32_t barlength = 1000; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + frame_count = 0; + start_time = grub_get_time_ms (); + barstart = grub_get_time_ms (); + + while (grub_checkkey () == -1) + { + grub_uint64_t now; + grub_uint32_t bartime; + now = grub_get_time_ms (); + /* If the time limit option is set, then check if it's exceeded. */ + if (vt_opts->test_time != 0 + && now >= start_time + vt_opts->test_time * 1000) + break; + bartime = now - barstart; + if (bartime > barlength) + { + barstart = grub_get_time_ms (); /* Start over. */ + bartime = barlength; + } + + /* Clear screen. */ + grub_video_fill_rect (bgcolor, view.x, view.y, view.width, view.height); + + /* Border. */ + grub_video_fill_rect (framecolor, + bar_frame.x - 1, bar_frame.y - 1, + bar_frame.width + 2, bar_frame.height + 2); + + /* Bar background. */ + int barwidth = bar_frame.width * bartime / barlength; + grub_video_fill_rect (barbgcolor, bar_frame.x + barwidth, + bar_frame.y, bar_frame.width - barwidth, + bar_frame.height); + /* Bar foreground. */ + grub_video_fill_rect (barcolor, bar_frame.x, bar_frame.y, + barwidth, bar_frame.height); + grub_video_swap_buffers (); + frame_count++; + } + + grub_uint32_t duration = grub_get_time_ms () - start_time; + grub_uint32_t fps_x10 = 10 * 1000 * frame_count / duration; + + if (grub_checkkey () != -1) + grub_getkey (); /* Eat the keypress, if there was one. */ + grub_video_restore (); + grub_printf ("Average frame rate: %d.%d fps\n", fps_x10 / 10, fps_x10 % 10); +} + + +/** + * Test double buffering. + */ +static void +doublebuf_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (640, 480, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + grub_video_color_t red = grub_video_map_rgb (255, 50, 50); + grub_video_color_t yellow = grub_video_map_rgb (255, 255, 0); + grub_video_color_t green = grub_video_map_rgb (20, 255, 20); + grub_video_color_t blue = grub_video_map_rgb (50, 50, 255); + grub_video_color_t black = grub_video_map_rgb (0, 0, 0); + grub_video_color_t bgcolor = grub_video_map_rgb (255, 255, 255); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (red, 100, 100, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (yellow, 120, 120, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (green, 140, 140, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (blue, 160, 160, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_fill_rect (bgcolor, 0, 0, 640, 480); + grub_video_fill_rect (black, 180, 180, 200, 200); + grub_video_swap_buffers (); + grub_getkey (); + + grub_video_restore (); +} + + +/** + * Test text rendering. + */ +static void +text_test (struct videotest_options *vt_opts) +{ + grub_video_color_t color; + const char *s; + int view_x; + int view_y; + int view_width; + int view_height; + int xpos; + int ypos; + int i; + grub_font_t font1; + grub_font_t font2; + grub_font_t font3; + + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_video_get_viewport ((unsigned int *) &view_x, + (unsigned int *) &view_y, + (unsigned int *) &view_width, + (unsigned int *) &view_height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + if (!(font1 = grub_font_get ("New Century Schoolbook 24")) + || !(font2 = grub_font_get ("Helvetica Bold 14")) + || !(font3 = grub_font_get ("Helvetica 10"))) + { + grub_video_restore (); + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + color = grub_video_map_rgb (0, 0, 0); + grub_video_fill_rect (color, 0, 0, view_width, view_height); + + color = grub_video_map_rgb (255, 0, 0); + grub_video_fill_rect (color, 0, 0, 100, 100); + + color = grub_video_map_rgb (0, 255, 255); + grub_video_fill_rect (color, 100, 100, 100, 100); + + color = grub_video_map_rgb (255, 255, 255); + + xpos = 10; + ypos = 30; + s = "Hello, World!"; + for (i = 0; i < 40; i++) + { + if (xpos + grub_font_get_string_width (font1, s) >= view_width) + { + /* The string will wrap; go to the beginning of the next line. */ + xpos = 10; + ypos += (grub_font_get_descent (font1) + + grub_font_get_ascent (font1)); + } + grub_video_draw_string ("Hello, World!", + font1, grub_video_map_rgb (255, 255, 0), + view_x + xpos, view_y + ypos); + + xpos += grub_font_get_string_width (font1, s); + } + + xpos = 300; + ypos = 450; + grub_video_draw_string (grub_font_get_name (font1), + font1, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + ypos += grub_font_get_descent (font1) + grub_font_get_ascent (font2) + 2; + grub_video_draw_string (grub_font_get_name (font2), + font2, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + ypos += grub_font_get_descent (font2) + grub_font_get_ascent (font3) + 2; + grub_video_draw_string (grub_font_get_name (font3), + font3, grub_video_map_rgb (255, 255, 255), + view_x + xpos, view_y + ypos); + + grub_video_swap_buffers (); + grub_getkey (); + grub_video_restore (); + grub_errno = GRUB_ERR_NONE; +} + + +/** + * Test bitmap scaling. + */ +static void +scale_test (struct videotest_options *vt_opts) +{ + int mode_type = GRUB_VIDEO_MODE_TYPE_RGB; + if (vt_opts->double_buffering) + mode_type |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + if (grub_video_setup (1024, 768, mode_type) != GRUB_ERR_NONE) + return; + + grub_errno = GRUB_ERR_NONE; + grub_video_rect_t view; + grub_video_get_viewport ((unsigned *) &view.x, (unsigned *) &view.y, + (unsigned *) &view.width, + (unsigned *) &view.height); + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + grub_font_t font; + if (!(font = grub_font_get ("Helvetica 10"))) + { + grub_video_restore (); + grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); + return; + } + + grub_video_color_t color; + + int text_y = 0; + int text_height = 25; + + color = grub_video_map_rgb (44, 44, 200); + grub_video_fill_rect (color, view.x, view.y, view.width, view.height); + color = grub_video_map_rgb (255, 255, 255); + + grub_video_draw_string ("Loading image", + font, color, 10, text_y += text_height); + + enum grub_video_bitmap_scale_method scale_method = + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST; + const char *bitmap_name = "/boot/grub/themes/winter/without-leaves.png"; + struct grub_video_bitmap *bitmap; + grub_video_bitmap_load (&bitmap, bitmap_name); + if (grub_errno != GRUB_ERR_NONE) + { + grub_video_draw_string ("Error loading bitmap", + font, color, 10, text_y += text_height); + } + else + { + grub_video_draw_string ("Original image", + font, color, 10, text_y += text_height); + grub_video_blit_bitmap (bitmap, GRUB_VIDEO_BLIT_BLEND, + 400, text_y - 10, + 0, 0, grub_video_bitmap_get_width (bitmap), + grub_video_bitmap_get_height (bitmap)); + + struct grub_video_bitmap *bitmap2; + if (grub_video_bitmap_create_scaled (&bitmap2, 40, 40, + bitmap, + scale_method) + != GRUB_ERR_NONE) + { + grub_video_draw_string ("Error scaling down bitmap", + font, color, 10, text_y += text_height); + } + else + { + grub_video_draw_string ("Scaled down version", + font, color, 10, text_y += text_height); + grub_video_blit_bitmap (bitmap2, GRUB_VIDEO_BLIT_BLEND, + 400, text_y + 100, + 0, 0, grub_video_bitmap_get_width (bitmap2), + grub_video_bitmap_get_height (bitmap2)); + grub_video_bitmap_destroy (bitmap2); + } + + struct grub_video_bitmap *bitmap3; + if (grub_video_bitmap_create_scaled (&bitmap3, 500, 300, bitmap, + scale_method) + != GRUB_ERR_NONE) + { + grub_video_draw_string ("Error scaling up bitmap", + font, color, 10, text_y += text_height); + } + else + { + grub_video_draw_string ("Scaled up version", + font, color, 10, text_y += text_height); + grub_video_blit_bitmap (bitmap3, GRUB_VIDEO_BLIT_BLEND, + 400, text_y + 50, + 0, 0, grub_video_bitmap_get_width (bitmap3), + grub_video_bitmap_get_height (bitmap3)); + grub_video_bitmap_destroy (bitmap3); + } + } + + grub_video_swap_buffers (); + grub_getkey (); + grub_video_bitmap_destroy (bitmap); + grub_video_restore (); +} + + +/** Print a list of the available tests. */ +static void list_tests (void); + +/** + * Video test command. Takes an argument specifying the test to run. + */ +static grub_err_t +grub_cmd_videotest (struct grub_arg_list *state, int argc, char **args) +{ + int i; + struct videotest_options vt_opts; + /* Pointer to the test function. */ + void (*test_func) (struct videotest_options *) = NULL; + + vt_opts.test_time = + state[ARGINDEX_TEST_TIME].set + ? grub_strtoul (state[ARGINDEX_TEST_TIME].arg, 0, 0) : 0; + vt_opts.double_buffering = state[ARGINDEX_DOUBLE_BUF].set; + + /* Parse command line arguments to determine the test to run. */ + for (i = 0; i < argc; i++) + { + char *arg = args[i]; + if (grub_strcmp (arg, "list") == 0) + { + list_tests (); + return GRUB_ERR_NONE; + } + else if (grub_strcmp (arg, "basic") == 0) + { + test_func = basic_video_test; + } + else if (grub_strcmp (arg, "bench") == 0) + { + test_func = benchmark_test; + } + else if (grub_strcmp (arg, "bitmaps") == 0) + { + test_func = bitmap_demo; + } + else if (grub_strcmp (arg, "clock") == 0) + { + test_func = clock_test; + } + else if (grub_strcmp (arg, "doublebuf") == 0) + { + test_func = doublebuf_test; + } + else if (grub_strcmp (arg, "text") == 0) + { + test_func = text_test; + } + else if (grub_strcmp (arg, "scale") == 0) + { + test_func = scale_test; + } + else + { + grub_printf ("Error: Unknown test `%s'\n", arg); + grub_errno = GRUB_ERR_BAD_ARGUMENT; + return grub_errno; + } + } + + if (test_func == NULL) + { + grub_printf ("Usage: videotest TESTNAME Run a test.\n"); + grub_printf (" videotest list List available tests.\n"); + grub_printf ("\n"); + list_tests (); + } + else + { + test_func (&vt_opts); + } + return grub_errno; } -GRUB_MOD_INIT(videotest) +static void +list_tests (void) +{ + grub_printf ("Available tests\n"); + grub_printf ("===============\n"); + grub_printf ("basic Basic video test with filled rectangles,\n"); + grub_printf (" offscreen rendering targets, some text.\n"); + grub_printf ("bench Run a performance benchmark.\n"); + grub_printf ("bitmaps Test generating and blitting bitmaps.\n"); + grub_printf ("clock Test time functions w/ animated progress bar.\n"); + grub_printf ("doublebuf Test double buffering.\n"); + grub_printf ("text Test text rendering.\n"); + grub_printf ("scale Test image scaling.\n"); + grub_printf ("\n"); +} + + +GRUB_MOD_INIT (videotest) { grub_register_command ("videotest", grub_cmd_videotest, GRUB_COMMAND_FLAG_BOTH, - "videotest", - "Test video subsystem", - 0); + "videotest TEST", + "Run the specified video subsystem test.", + arg_options); } -GRUB_MOD_FINI(videotest) +GRUB_MOD_FINI (videotest) { grub_unregister_command ("videotest"); } === modified file 'conf/common.rmk' --- conf/common.rmk 2008-08-09 11:30:26 +0000 +++ conf/common.rmk 2008-08-11 14:46:59 +0000 @@ -281,6 +281,7 @@ cmp.mod cat.mod help.mod font.mod search.mod \ loopback.mod fs_uuid.mod configfile.mod echo.mod \ terminfo.mod test.mod blocklist.mod hexdump.mod \ + gfxmenu.mod \ read.mod sleep.mod loadenv.mod crc.mod # For hello.mod. @@ -288,6 +289,23 @@ hello_mod_CFLAGS = $(COMMON_CFLAGS) hello_mod_LDFLAGS = $(COMMON_LDFLAGS) +# For gfxmenu.mod. +gfxmenu_mod_SOURCES = \ + gfxmenu/gfxmenu.c \ + gfxmenu/model.c \ + gfxmenu/view.c \ + gfxmenu/theme_loader.c \ + gfxmenu/widget-box.c \ + gfxmenu/gui_canvas.c \ + gfxmenu/gui_box.c \ + gfxmenu/gui_label.c \ + gfxmenu/gui_image.c \ + gfxmenu/gui_progress_bar.c \ + gfxmenu/gui_util.c \ + gfxmenu/stringutil.c +gfxmenu_mod_CFLAGS = $(COMMON_CFLAGS) +gfxmenu_mod_LDFLAGS = $(COMMON_LDFLAGS) + # For boot.mod. boot_mod_SOURCES = commands/boot.c boot_mod_CFLAGS = $(COMMON_CFLAGS) @@ -324,7 +342,7 @@ help_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/i386-pc.rmk' --- conf/i386-pc.rmk 2008-08-05 19:24:00 +0000 +++ conf/i386-pc.rmk 2008-08-11 14:46:59 +0000 @@ -48,11 +48,13 @@ kern/generic/rtc_get_time_ms.c \ kern/generic/millisleep.c \ kern/env.c \ + kern/menu_viewer.c \ term/i386/pc/console.c \ symlist.c kernel_img_HEADERS = arg.h boot.h cache.h device.h disk.h dl.h elf.h elfload.h \ env.h err.h file.h fs.h kernel.h loader.h misc.h mm.h net.h parser.h \ partition.h pc_partition.h rescue.h symbol.h term.h time.h types.h \ + menu_viewer.h \ machine/biosdisk.h machine/boot.h machine/console.h machine/init.h \ machine/memory.h machine/loader.h machine/vga.h machine/vbe.h \ machine/kernel.h machine/pxe.h @@ -251,7 +253,7 @@ play_mod_LDFLAGS = $(COMMON_LDFLAGS) # For video.mod. -video_mod_SOURCES = video/video.c +video_mod_SOURCES = video/video.c video/setmode.c video_mod_CFLAGS = $(COMMON_CFLAGS) video_mod_LDFLAGS = $(COMMON_LDFLAGS) @@ -266,7 +268,10 @@ videotest_mod_LDFLAGS = $(COMMON_LDFLAGS) # For bitmap.mod -bitmap_mod_SOURCES = video/bitmap.c +bitmap_mod_SOURCES = video/bitmap.c \ + video/bitmap_scale_nn.c \ + video/bitmap_scale_bilinear.c \ + video/bitmap_scale.c bitmap_mod_CFLAGS = $(COMMON_CFLAGS) bitmap_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'conf/sparc64-ieee1275.rmk' --- conf/sparc64-ieee1275.rmk 2008-08-05 10:54:37 +0000 +++ conf/sparc64-ieee1275.rmk 2008-08-07 23:48:19 +0000 @@ -196,7 +196,7 @@ cat_mod_LDFLAGS = $(COMMON_LDFLAGS) # For font.mod. -font_mod_SOURCES = font/manager.c +font_mod_SOURCES = font/font_cmd.c font/font.c font/loader.c font_mod_CFLAGS = $(COMMON_CFLAGS) font_mod_LDFLAGS = $(COMMON_LDFLAGS) === modified file 'config.h.in' --- config.h.in 2008-07-13 00:55:15 +0000 +++ config.h.in 2008-07-19 21:46:40 +0000 @@ -113,10 +113,37 @@ /* Number of bits in a file offset, on hosts where this is settable. */ #undef _FILE_OFFSET_BITS +/* Define for large files, on AIX-style hosts. */ +#undef _LARGE_FILES + +/* Define to 1 if on MINIX. */ +#undef _MINIX + +/* Define to 2 if the system does not provide POSIX.1 features except with + this defined. */ +#undef _POSIX_1_SOURCE + +/* Define to 1 if you need to in order for `stat' and other things to work. */ +#undef _POSIX_SOURCE + +/* Enable extensions on AIX 3, Interix. */ +#ifndef _ALL_SOURCE +# undef _ALL_SOURCE +#endif /* Enable GNU extensions on systems that have them. */ #ifndef _GNU_SOURCE # undef _GNU_SOURCE #endif +/* Enable threading extensions on Solaris. */ +#ifndef _POSIX_PTHREAD_SEMANTICS +# undef _POSIX_PTHREAD_SEMANTICS +#endif +/* Enable extensions on HP NonStop. */ +#ifndef _TANDEM_SOURCE +# undef _TANDEM_SOURCE +#endif +/* Enable general extensions on Solaris. */ +#ifndef __EXTENSIONS__ +# undef __EXTENSIONS__ +#endif -/* Define for large files, on AIX-style hosts. */ -#undef _LARGE_FILES === added file 'docs/gfxmenu-theme-example.txt' --- docs/gfxmenu-theme-example.txt 1970-01-01 00:00:00 +0000 +++ docs/gfxmenu-theme-example.txt 2008-08-11 14:38:31 +0000 @@ -0,0 +1,125 @@ +# GRUB gfxmenu theme "winter". +# Uses background image from: +# http://www.cyberpunkcafe.com/e107_plugins/autogallery/autogallery.php?show=1.Open%20Source%20Wallpaper +# "without-leaves.png" was called "Without Leafs in Winter.png" + +lua-script: "winter.lua" +title-text: "" +title-font: "Helvetica Bold 18" +item-font: "Helvetica Bold 14" +selected-item-font: "Helvetica Bold 14" +status-font: "Helvetica 8" +terminal-font: "Fixed 9" +title-color: "40, 40, 40" +item-color: "0, 0, 0" +selected-item-color: "203, 251, 255" +status-color: "#FFF" +status-bg-color: "0, 166, 183, 128" +desktop-image: "without-leaves.png" +desktop-color: "0, 154, 183" + +menu-box: "menu_*.png" +selected-item-box: "select_*.png" +terminal-box: "terminal_*.png" + +icon-width: 44 +icon-height: 44 +item-height: 32 +item-padding: 0 +item-icon-space: 3 +item-spacing: 11 + +# You can add text at arbitrary locations on the screen. +# The specification within the "+label {...}" block is free-form, +# so you can use as much or as little white space as you like. + ++ label { + position = (170, 50) + font = "smoothansi 13" + color = "0,0,128" + text = "This is the Winter theme ... brought to you by GRUB!" +} + +# Show the text alignment supported by labels. ++ vbox { + position = (220, 347) + preferred_size = (200, -1) # A preferred size of -1 means automatic. + + label { text="Text alignment demo" align="center" font="aqui 11" } + + label { text="Left" align="left" font="cure 11" } + + label { text="Center" align="center" font="cure 11" } + + label { text="Right" align="right" font="cure 11" } +} + ++ vbox { + position = (580, 10) + + label { text="GNU" font="gelly 11" color="0, 0, 0" } + + label { text="GRUB" font="aqui 11" color="0, 0, 0" } + + label { text="boot loader" font="cure 11" color="0, 0, 0" } +} + ++ hbox { + position = (80, 10) + + label { text="GNU" font="gelly 11" color="0, 0, 0" } + + label { text="GRUB" font="aqui 11" color="0, 0, 0" } + + label { text="boot loader" font="cure 11" color="0, 0, 0" } +} + +# Demonstration of a compound layout: boxes within boxes. ++ hbox +{ + position = (480, 3) + + + vbox + { + # Note: We can't just use 'size' to set the image's size, + # since the vbox will resize the component according to its + # preferred size, which for images is the native image size. + + + image { file="/boot/grub/themes/icons/ubuntu.png" + preferred_size = (20, 20) } + + image { file="/boot/grub/themes/icons/gentoo.png" + preferred_size = (20, 20) } + } + + + vbox + { + + label { text="GRand" font="cure 11" color=#99F } + + label { text="Unified" font="cure 11" color=#BBF } + + label { text="Bootloader" font="cure 11" color=#DDF } + } +} + +# By defining a 'progress_bar' type component with an ID of '__timeout__', +# the progress bar will be used to display the time remaining before an +# the default entry is automatically booted. ++ progress_bar +{ + id = "__timeout__" + position = (80, 393) + size = (500, 24) + font = "cure 11" + text_color = #000 + fg_color = #CCF + bg_color = #66B + border_color = #006 + show_text = false +} + +# Although the progress_bar component is normally used to indicate the +# time remaining, it's also possible to create other components with an ID +# of '__timeout__'. All components with and ID of 'timeout_bar' will have +# the following properties set based on the timeout value: +# text, value, start, end, visible. +# In this case, we have set 'show_text=false' on the progress bar, and use +# the following label's 'text' property to display the message. ++ label +{ + id = "__timeout__" + position = (80, 420) + size = (500, 24) + font = "lime 11" + color = #117 + align = "center" +} + + === added file 'font/font.c' --- font/font.c 1970-01-01 00:00:00 +0000 +++ font/font.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,92 @@ +/* font.c - Font functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +grub_font_t +grub_font_get (const char *font_name) +{ + struct font_node *node; + + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + if (grub_strcmp (font->name, font_name) == 0) + return font; + } + + /* If no font by that name is found, return the first font in the list + * as a fallback. */ + return grub_font_list->value; +} + +const char * +grub_font_get_name (grub_font_t font) +{ + return font->name; +} + +int +grub_font_get_max_char_width (grub_font_t font) +{ + return font->max_char_width; +} + +int +grub_font_get_max_char_height (grub_font_t font) +{ + return font->max_char_height; +} + +int +grub_font_get_ascent (grub_font_t font) +{ + return font->ascent; +} + +int +grub_font_get_descent (grub_font_t font) +{ + return font->descent; +} + +int +grub_font_get_string_width (grub_font_t font, const char *str) +{ + int i; + int width; + struct grub_font_glyph *glyph; + grub_size_t len; + + len = grub_strlen (str); + + for (i = 0, width = 0; i < len; i++) + { + glyph = grub_font_get_glyph (font, str[i]); + width += glyph->device_width; + } + + return width; +} === added file 'font/font_cmd.c' --- font/font_cmd.c 1970-01-01 00:00:00 +0000 +++ font/font_cmd.c 2008-07-03 14:12:08 +0000 @@ -0,0 +1,75 @@ +/* font_cmd.c - Font command definition. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include + +static grub_err_t +loadfont_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc, + char **args) +{ + if (argc == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); + + while (argc--) + if (grub_font_load (*args++) != 0) + return GRUB_ERR_BAD_FONT; + + return GRUB_ERR_NONE; +} + +static grub_err_t +lsfonts_command (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + struct font_node *node; + + grub_printf ("Loaded fonts:\n"); + for (node = grub_font_list; node; node = node->next) + { + grub_font_t font = node->value; + grub_printf ("%s\n", font->name); + } + + return GRUB_ERR_NONE; +} + +GRUB_MOD_INIT(font_manager) +{ + grub_font_loader_init (); + + grub_register_command ("loadfont", loadfont_command, GRUB_COMMAND_FLAG_BOTH, + "loadfont FILE...", + "Specify one or more font files to load.", 0); + + grub_register_command ("lsfonts", lsfonts_command, GRUB_COMMAND_FLAG_BOTH, + "lsfonts", + "List the loaded fonts.", 0); +} + +GRUB_MOD_FINI(font_manager) +{ + /* Should this free fonts, unknown_glyph, etc.? Freeing fonts could + * be a Bad Thing if there are still references to any of them. */ + + grub_unregister_command ("loadfont"); +} + === added file 'font/loader.c' --- font/loader.c 1970-01-01 00:00:00 +0000 +++ font/loader.c 2008-07-29 14:51:29 +0000 @@ -0,0 +1,689 @@ +/* loader.c - Functions to handle loading fonts and glyphs from files. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* Definition of font registry. */ +struct font_node *grub_font_list; + +static int register_font (grub_font_t font); +static void free_font (grub_font_t font); +static void remove_font (grub_font_t font); + +struct font_file_section +{ + grub_file_t file; /* The file this section is in. */ + char name[4]; /* FOURCC name of the section. */ + grub_uint32_t length; /* Length of the section contents. */ + int eof; /* Set by open_section() on EOF. */ +}; + +/* Font file format constants. */ +static const char pff2_magic[4] = { 'P', 'F', 'F', '2' }; +static const char section_names_file[4] = { 'F', 'I', 'L', 'E' }; +static const char section_names_font_name[4] = { 'N', 'A', 'M', 'E' }; +static const char section_names_max_char_width[4] = { 'M', 'A', 'X', 'W' }; +static const char section_names_max_char_height[4] = { 'M', 'A', 'X', 'H' }; +static const char section_names_ascent[4] = { 'A', 'S', 'C', 'E' }; +static const char section_names_descent[4] = { 'D', 'E', 'S', 'C' }; +static const char section_names_char_index[4] = { 'C', 'H', 'I', 'X' }; +static const char section_names_data[4] = { 'D', 'A', 'T', 'A' }; + +/* Replace unknown glyphs with a rounded question mark. */ +static grub_uint8_t unknown_glyph_bitmap[] = +{ + /* 76543210 */ + 0x7C, /* ooooo */ + 0x82, /* o o */ + 0xBA, /* o ooo o */ + 0xAA, /* o o o o */ + 0xAA, /* o o o o */ + 0x8A, /* o o o */ + 0x9A, /* o oo o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x92, /* o o o */ + 0x82, /* o o */ + 0x7C, /* ooooo */ + 0x00 /* */ +}; + +static struct grub_font_glyph *unknown_glyph; + +void +grub_font_loader_init (void) +{ + unknown_glyph = grub_malloc(sizeof(struct grub_font_glyph) + + sizeof(unknown_glyph_bitmap)); + if (! unknown_glyph) + return; + + unknown_glyph->width = 8; + unknown_glyph->height = 16; + unknown_glyph->offset_x = 0; + unknown_glyph->offset_y = 0; + unknown_glyph->device_width = 8; + grub_memcpy(unknown_glyph->bitmap, + unknown_glyph_bitmap, sizeof(unknown_glyph_bitmap)); +} + +/* + Open the next section in the file. + + On success, the section name is stored in section->name and the length in + section->length, and 0 is returned. On failure, 1 is returned and + grub_errno is set approriately with an error message. + + If 1 is returned due to being at the end of the file, then section->eof is + set to 1; otherwise, section->eof is set to 0. + */ +static int +open_section (grub_file_t file, struct font_file_section *section) +{ + grub_ssize_t retval; + grub_uint32_t raw_length; + + section->file = file; + section->eof = 0; + + /* Read the FOURCC section name. */ + retval = grub_file_read (file, section->name, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section name"); + return 1; /* Read error. */ + } + + /* Read the big-endian 32-bit section length. */ + retval = grub_file_read (file, (char *) &raw_length, 4); + if (retval >= 0 && retval < 4) + { + section->eof = 1; + return 1; /* EOF encountered. */ + } + else if (retval < 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font format error: can't read section length"); + return 1; /* Read error. */ + } + + /* Convert byte-order and store in *length. */ + section->length = grub_be_to_cpu32 (raw_length); + + return 0; +} + +/* Size in bytes of each character index (CHIX section) + entry in the font file. */ +#define FONT_CHAR_INDEX_ENTRY_SIZE (4 + 1 + 4) + +/* + Load the character index (CHIX) section contents from the font file. This + presumes that the position of FILE is positioned immediately after the + section length for the CHIX section (i.e., at the start of the section + contents). Returns 0 upon success, nonzero for failure (in which case + grub_errno is set appropriately). + */ +static int +load_font_index (grub_file_t file, grub_uint32_t sect_length, struct + grub_font *font) +{ + unsigned i; + +#if FONT_DEBUG >= 2 + grub_printf("load_font_index(sect_length=%d)\n", sect_length); +#endif + + /* Sanity check: ensure section length is divisible by the entry size. */ + if (sect_length % FONT_CHAR_INDEX_ENTRY_SIZE != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: character index length %d " + "is not a multiple of the entry size %d", + sect_length, FONT_CHAR_INDEX_ENTRY_SIZE); + return 1; /* Invalid index section length. */ + } + + /* Calculate the number of characters. */ + font->num_chars = sect_length / FONT_CHAR_INDEX_ENTRY_SIZE; + + /* Allocate the character index array. */ + font->char_index = grub_malloc (font->num_chars + * sizeof (struct char_index_entry)); + if (!font->char_index) + return 1; /* Error allocating memory. */ + +#if FONT_DEBUG >= 2 + grub_printf("num_chars=%d)\n", font->num_chars); +#endif + + /* Load the character index data from the file. */ + for (i = 0; i < font->num_chars; i++) + { + struct char_index_entry *entry = &font->char_index[i]; + + /* Read code point value; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->code, 4) != 4) + return 1; + entry->code = grub_be_to_cpu32 (entry->code); + + /* Read storage flags byte. */ + if (grub_file_read (file, (char *) &entry->storage_flags, 1) != 1) + return 1; + + /* Read glyph data offset; convert to native byte order. */ + if (grub_file_read (file, (char *) &entry->offset, 4) != 4) + return 1; + entry->offset = grub_be_to_cpu32 (entry->offset); + + /* No glyph loaded. Will be loaded on demand and cached thereafter. */ + entry->glyph = 0; + +#if FONT_DEBUG >= 5 + if (i < 10) /* Print the 1st 10 characters. */ + grub_printf("c=%d o=%d\n", entry->code, entry->offset); +#endif + } + + return 0; /* Index loaded OK. */ +} + +/* + Read the contents of the specified section as a string, which is + allocated on the heap. Returns 0 if there is an error. + */ +static char * +read_section_as_string (struct font_file_section *section) +{ + char *str; + grub_ssize_t ret; + + str = grub_malloc (section->length + 1); + if (!str) + return 0; + + ret = grub_file_read (section->file, str, section->length); + if (ret < 0 || ret != (grub_ssize_t) section->length) + { + grub_free (str); + return 0; + } + + str[section->length] = '\0'; + return str; +} + +/* + Read the contents of the current section as a 16-bit integer value, + which is stored into *VALUE. Returns 0 upon success, nonzero upon failure. + */ +static int +read_section_as_short (struct font_file_section *section, grub_int16_t *value) +{ + grub_uint16_t raw_value; + + if (section->length != 2) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: section %c%c%c%c length " + "is %d but should be 2", + section->name[0], section->name[1], + section->name[2], section->name[3], + section->length); + return 1; /* An error occurred. */ + } + if (grub_file_read (section->file, (char *) &raw_value, 2) != 2) + return 1; /* An error occurred. */ + + *value = grub_be_to_cpu16 (raw_value); + return 0; /* Successfully read the value. */ +} + +/* + Load a font and add it to the beginning of the global font list. + Returns 0 upon success, nonzero upon failure. + */ +int +grub_font_load (const char *filename) +{ + grub_file_t file = 0; + struct font_file_section section; + char magic[4]; + grub_font_t font = 0; + +#if FONT_DEBUG >= 1 + grub_printf("add_font(%s)\n", filename); +#endif + + file = grub_buffile_open (filename, 1024); + if (!file) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("file opened\n"); +#endif + + /* Read the FILE section. It indicates the file format. */ + if (open_section (file, §ion) != 0) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("opened FILE section\n"); +#endif + if (grub_memcmp (section.name, section_names_file, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error: 1st section must be FILE"); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section name ok\n"); +#endif + if (section.length != 4) + { + grub_error (GRUB_ERR_BAD_FONT, + "Font file format error (file type ID length is %d " + "but should be 4)", section.length); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("section length ok\n"); +#endif + /* Check the file format type code. */ + if (grub_file_read (file, magic, 4) != 4) + goto fail; + +#if FONT_DEBUG >= 3 + grub_printf("read magic ok\n"); +#endif + + if (grub_memcmp (magic, pff2_magic, 4) != 0) + { + grub_error (GRUB_ERR_BAD_FONT, "Invalid font magic %x %x %x %x", + magic[0], magic[1], magic[2], magic[3]); + goto fail; + } + +#if FONT_DEBUG >= 3 + grub_printf("compare magic ok\n"); +#endif + + /* Allocate the font object. */ + font = (grub_font_t) grub_malloc (sizeof (struct grub_font)); + if (!font) + goto fail; + + font->file = file; + font->name = 0; + font->ascent = 0; + font->descent = 0; + font->max_char_width = 0; + font->max_char_height = 0; + font->num_chars = 0; + font->char_index = 0; + +#if FONT_DEBUG >= 3 + grub_printf("allocate font ok; loading font info\n"); +#endif + + /* Load the font information. */ + while (1) + { + if (open_section (file, §ion) != 0) + { + if (section.eof) + break; /* Done reading the font file. */ + else + goto fail; + } + +#if FONT_DEBUG >= 2 + grub_printf("opened section %c%c%c%c ok\n", + section.name[0], section.name[1], + section.name[2], section.name[3]); +#endif + + if (grub_memcmp (section.name, section_names_font_name, 4) == 0) + { + font->name = read_section_as_string (§ion); + if (!font->name) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_width, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_width) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_max_char_height, 4) == 0) + { + if (read_section_as_short (§ion, &font->max_char_height) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_ascent, 4) == 0) + { + if (read_section_as_short (§ion, &font->ascent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_descent, 4) == 0) + { + if (read_section_as_short (§ion, &font->descent) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_char_index, 4) == 0) + { + /* Load the font index. */ + if (load_font_index (file, section.length, font) != 0) + goto fail; + } + else if (grub_memcmp (section.name, section_names_data, 4) == 0) + { + /* When the DATA section marker is reached, we stop reading. */ + break; + } + else + { + /* Unhandled section type, simply skip past it. */ +#if FONT_DEBUG >= 3 + grub_printf("Unhandled section type, skipping.\n"); +#endif + grub_off_t section_end = grub_file_tell (file) + section.length; + if ((int) grub_file_seek (file, section_end) == -1) + goto fail; + } + } + + if (!font->name) + { + grub_printf ("Note: Font has no name.\n"); + font->name = grub_strdup ("Unknown"); + } + +#if FONT_DEBUG >= 1 + grub_printf ("Loaded font `%s'.\n" + "Ascent=%d Descent=%d MaxW=%d MaxH=%d Number of characters=%d.\n", + font->name, + font->ascent, font->descent, + font->max_char_width, font->max_char_height, + font->num_chars); +#endif + + if (font->max_char_width == 0 + || font->max_char_height == 0 + || font->num_chars == 0 + || font->char_index == 0 + || font->ascent == 0 + || font->descent == 0) + { + grub_error (GRUB_ERR_BAD_FONT, + "Invalid font file: missing some required data."); + goto fail; + } + + /* Add the font to the global font registry. */ + if (register_font (font) != 0) + goto fail; + + return 0; /* Font loaded ok. */ + +fail: + free_font (font); + return 1; /* Failed to load font. */ +} + +/* + Read a 16-bit big-endian integer from FILE, convert it to native byte + order, and store it in *VALUE. + Returns 0 on success, 1 on failure. + */ +static int +read_be_uint16 (grub_file_t file, grub_uint16_t * value) +{ + if (grub_file_read (file, (char *) value, 2) != 2) + return 1; + *value = grub_be_to_cpu16 (*value); + return 0; +} + +static int +read_be_int16 (grub_file_t file, grub_int16_t * value) +{ + /* For the signed integer version, use the same code as for unsigned. */ + return read_be_uint16 (file, (grub_uint16_t *) value); +} + +/* + Return a pointer to the character index entry for the glyph corresponding to + the codepoint CODE in the font FONT. If not found, return zero. + */ +static struct char_index_entry * +find_glyph (const grub_font_t font, grub_uint32_t code) +{ + grub_uint32_t i; + grub_uint32_t len = font->num_chars; + struct char_index_entry *table = font->char_index; + + /* Do a linear search. */ + for (i = 0; i < len; i++) + { + if (table[i].code == code) + return &table[i]; + } + + return 0; /* No entry found for code point CODE. */ +} + +static struct grub_font_glyph * +grub_font_get_glyph_internal (grub_font_t font, grub_uint32_t code) +{ + struct char_index_entry *index_entry; + + index_entry = find_glyph (font, code); + if (index_entry) + { + struct grub_font_glyph *glyph = 0; + grub_uint16_t width; + grub_uint16_t height; + grub_int16_t xoff; + grub_int16_t yoff; + grub_int16_t dwidth; + int len; + + if (index_entry->glyph) + return index_entry->glyph; /* Return cached glyph. */ + + /* Make sure we can find glyphs for error messages. Push active + error message to error stack and reset error message. */ + grub_error_push (); + + grub_file_seek (font->file, index_entry->offset); + + /* Read the glyph width, height, and baseline. */ + if (read_be_uint16(font->file, &width) != 0 + || read_be_uint16(font->file, &height) != 0 + || read_be_int16(font->file, &xoff) != 0 + || read_be_int16(font->file, &yoff) != 0 + || read_be_int16(font->file, &dwidth) != 0) + { + //remove_font (font); + return 0; + } + + len = (width * height + 7) / 8; + glyph = grub_malloc (sizeof (struct grub_font_glyph) + len); + if (! glyph) + { + //remove_font (font); + return 0; + } + + glyph->font = font; + glyph->width = width; + glyph->height = height; + glyph->offset_x = xoff; + glyph->offset_y = yoff; + glyph->device_width = dwidth; + + /* Don't try to read empty bitmaps (e.g., space characters). */ + if (len != 0) + { + if (grub_file_read (font->file, (char *) glyph->bitmap, len) != len) + { + //remove_font (font); + return 0; + } + } + + /* Restore old error message. */ + grub_error_pop (); + + /* Cache the glyph. */ + index_entry->glyph = glyph; + + return glyph; /* Glyph loaded ok. */ + } + + return 0; +} + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + Returns a pointer to an glyph indicating there is no glyph available + if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph * +grub_font_get_glyph (grub_font_t font, grub_uint32_t code) +{ + struct grub_font_glyph *glyph; + glyph = grub_font_get_glyph_internal (font, code); + if (glyph == 0) + glyph = unknown_glyph; + return glyph; +} + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + for CODE in the available fonts, then a glyph representing an unknown + character is returned. This function never returns NULL. + The returned glyph is owned by the font manager and should not be freed + by the caller. The glyphs are cached. */ +struct grub_font_glyph * +grub_font_get_glyph_any (grub_uint32_t code) +{ + struct font_node *node; + /* Keep track of next node, in case there's an I/O error in + grub_font_get_glyph() and the font is removed from the list. */ + struct font_node *next; + + for (node = grub_font_list; node; node = next) + { + grub_font_t font; + struct grub_font_glyph *glyph; + + font = node->value; + next = node->next; + + glyph = grub_font_get_glyph_internal (font, code); + if (glyph) + return glyph; + } + + /* Uggh... Glyph was not found in any font. */ + return unknown_glyph; /* Failed to load glyph. */ +} + +/* + Free the memory used by a font. + This should not be called if the font has been made available to + users (once it is added to the global font list), since there would + be the possibility of a dangling pointer. + */ +static void +free_font (grub_font_t font) +{ + if (font) + { + if (font->file) + grub_file_close (font->file); + if (font->name) + grub_free (font->name); + if (font->char_index) + grub_free (font->char_index); + grub_free (font); + } +} + +/* + Add FONT to the global font registry. + Returns 0 upon success, nonzero on failure (the font was not registered). + */ +static int +register_font (grub_font_t font) +{ + struct font_node *node = 0; + + node = grub_malloc (sizeof (struct font_node)); + if (!node) + return 1; /* Error. */ + + node->value = font; + node->next = grub_font_list; + grub_font_list = node; + + return 0; /* Success. */ +} + +/* + Remove the font from the global font list. We don't actually free the + font's memory since users could be holding references to the font. + */ +static void +remove_font (grub_font_t font) +{ + struct font_node **nextp, *cur; + + for (nextp = &grub_font_list, cur = *nextp; + cur; + nextp = &cur->next, cur = cur->next) + { + if (cur->value == font) + { + *nextp = cur->next; + + /* Free the node, but not the font itself. */ + grub_free (cur); + + return; + } + } +} + === removed file 'font/manager.c' --- font/manager.c 2008-08-01 03:06:55 +0000 +++ font/manager.c 1970-01-01 00:00:00 +0000 @@ -1,283 +0,0 @@ -/* - * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2005,2006,2007 Free Software Foundation, Inc. - * - * GRUB is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * GRUB is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with GRUB. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -struct entry -{ - grub_uint32_t code; - grub_uint32_t offset; -}; - -struct font -{ - struct font *next; - grub_file_t file; - grub_uint32_t num; - struct entry table[0]; -}; - -static struct font *font_list; - -/* Fill unknown glyph's with rounded question mark. */ -static grub_uint8_t unknown_glyph[16] = -{ /* 76543210 */ - 0x7C, /* ooooo */ - 0x82, /* o o */ - 0xBA, /* o ooo o */ - 0xAA, /* o o o o */ - 0xAA, /* o o o o */ - 0x8A, /* o o o */ - 0x9A, /* o oo o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x92, /* o o o */ - 0x82, /* o o */ - 0x7C, /* ooooo */ - 0x00 /* */ -}; - -static int -add_font (const char *filename) -{ - grub_file_t file = 0; - char magic[4]; - grub_uint32_t num, i; - struct font *font = 0; - - file = grub_buffile_open (filename, 0); - if (! file) - goto fail; - - if (grub_file_read (file, magic, 4) != 4) - goto fail; - - if (grub_memcmp (magic, GRUB_FONT_MAGIC, 4) != 0) - { - grub_error (GRUB_ERR_BAD_FONT, "invalid font magic"); - goto fail; - } - - if (grub_file_read (file, (char *) &num, 4) != 4) - goto fail; - - num = grub_le_to_cpu32 (num); - font = (struct font *) grub_malloc (sizeof (struct font) - + sizeof (struct entry) * num); - if (! font) - goto fail; - - font->file = file; - font->num = num; - - for (i = 0; i < num; i++) - { - grub_uint32_t code, offset; - - if (grub_file_read (file, (char *) &code, 4) != 4) - goto fail; - - if (grub_file_read (file, (char *) &offset, 4) != 4) - goto fail; - - font->table[i].code = grub_le_to_cpu32 (code); - font->table[i].offset = grub_le_to_cpu32 (offset); - } - - font->next = font_list; - font_list = font; - - return 1; - - fail: - if (font) - grub_free (font); - - if (file) - grub_file_close (file); - - return 0; -} - -static void -remove_font (struct font *font) -{ - struct font **p, *q; - - for (p = &font_list, q = *p; q; p = &(q->next), q = q->next) - if (q == font) - { - *p = q->next; - - grub_file_close (font->file); - grub_free (font); - - break; - } -} - -/* Return the offset of the glyph corresponding to the codepoint CODE - in the font FONT. If no found, return zero. */ -static grub_uint32_t -find_glyph (const struct font *font, grub_uint32_t code) -{ - grub_uint32_t start = 0; - grub_uint32_t end = font->num - 1; - const struct entry *table = font->table; - - /* This shouldn't happen. */ - if (font->num == 0) - return 0; - - /* Do a binary search. */ - while (start <= end) - { - grub_uint32_t i = (start + end) / 2; - - if (table[i].code < code) - start = i + 1; - else if (table[i].code > code) - end = i - 1; - else - return table[i].offset; - } - - return 0; -} - -/* Set the glyph to something stupid. */ -static void -fill_with_default_glyph (grub_font_glyph_t glyph) -{ - unsigned i; - - /* Use pre-defined pattern to fill unknown glyphs. */ - for (i = 0; i < 16; i++) - glyph->bitmap[i] = unknown_glyph[i]; - - glyph->char_width = 1; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; -} - -/* Get a glyph corresponding to the codepoint CODE. Always fill glyph - information with something, even if no glyph is found. */ -int -grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph) -{ - struct font *font; - grub_uint8_t bitmap[32]; - - /* FIXME: It is necessary to cache glyphs! */ - - restart: - for (font = font_list; font; font = font->next) - { - grub_uint32_t offset; - - offset = find_glyph (font, code); - if (offset) - { - grub_uint32_t w; - int len; - - /* Make sure we can find glyphs for error messages. Push active - error message to error stack and reset error message. */ - grub_error_push (); - - grub_file_seek (font->file, offset); - if ((len = grub_file_read (font->file, (char *) &w, sizeof (w))) - != sizeof (w)) - { - remove_font (font); - goto restart; - } - - w = grub_le_to_cpu32 (w); - if (w != 1 && w != 2) - { - /* grub_error (GRUB_ERR_BAD_FONT, "invalid width"); */ - remove_font (font); - goto restart; - } - - if (grub_file_read (font->file, (char *) bitmap, w * 16) - != (grub_ssize_t) w * 16) - { - remove_font (font); - goto restart; - } - - /* Fill glyph with information. */ - grub_memcpy (glyph->bitmap, bitmap, w * 16); - - glyph->char_width = w; - glyph->width = glyph->char_width * 8; - glyph->height = 16; - glyph->baseline = (16 * 3) / 4; - - /* Restore old error message. */ - grub_error_pop (); - - return 1; - } - } - - /* Uggh... No font was found. */ - fill_with_default_glyph (glyph); - return 0; -} - -static grub_err_t -font_command (struct grub_arg_list *state __attribute__ ((unused)), - int argc __attribute__ ((unused)), - char **args __attribute__ ((unused))) -{ - if (argc == 0) - return grub_error (GRUB_ERR_BAD_ARGUMENT, "no font specified"); - - while (argc--) - if (! add_font (*args++)) - return 1; - - return 0; -} - -GRUB_MOD_INIT(font_manager) -{ - grub_register_command ("font", font_command, GRUB_COMMAND_FLAG_BOTH, - "font FILE...", - "Specify one or more font files to display.", 0); -} - -GRUB_MOD_FINI(font_manager) -{ - grub_unregister_command ("font"); -} === modified file 'genmk.rb' --- genmk.rb 2008-07-28 21:35:40 +0000 +++ genmk.rb 2008-08-03 03:57:46 +0000 @@ -126,7 +126,7 @@ $(TARGET_CC) $(TARGET_CPPFLAGS) $(TARGET_CFLAGS) $(#{prefix}_CFLAGS) -c -o $@ $< #{mod_src}: moddep.lst genmodsrc.sh - sh $(srcdir)/genmodsrc.sh '#{mod_name}' $< > $@ || (rm -f $@; exit 1) + $(GENMODSRC) '#{mod_name}' $< > $@ || (rm -f $@; exit 1) ifneq ($(#{prefix}_EXPORTS),no) #{defsym}: #{pre_obj} @@ -158,18 +158,21 @@ PARTMAPFILES += #{partmap} #{command}: #{src} $(#{src}_DEPENDENCIES) gencmdlist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENCMDLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/gencmdlist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{fs}: #{src} $(#{src}_DEPENDENCIES) genfslist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENFSLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genfslist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) #{partmap}: #{src} $(#{src}_DEPENDENCIES) genpartmaplist.sh - set -e; \ - $(TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ + $(INFO_GENPARTMAPLIST) + $(V_PREFIX)set -e; \ + $(_TARGET_CC) -I#{dir} -I$(srcdir)/#{dir} $(TARGET_CPPFLAGS) $(TARGET_#{flag}) $(#{prefix}_#{flag}) -E $< \ | sh $(srcdir)/genpartmaplist.sh #{symbolic_name} > $@ || (rm -f $@; exit 1) === added directory 'gfxmenu' === added file 'gfxmenu/gfxmenu.c' --- gfxmenu/gfxmenu.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gfxmenu.c 2008-07-27 00:06:20 +0000 @@ -0,0 +1,221 @@ +/* gfxmenu.c - Graphical menu interface controller. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void switch_to_text_menu (void) +{ + grub_env_set ("menuviewer", "terminal"); +} + +static void +process_key_press (int c, + grub_gfxmenu_model_t model, + grub_gfxmenu_view_t view, + int nested, + int *should_exit) +{ + /* When a key is pressed, stop the timeout. */ + grub_gfxmenu_model_clear_timeout (model); + + if (c == 'j' || c == GRUB_TERM_DOWN) + { + int i = grub_gfxmenu_model_get_selected_index (model); + int num_items = grub_gfxmenu_model_get_num_entries (model); + if (i < num_items - 1) + { + i++; + grub_gfxmenu_model_set_selected_index (model, i); + } + } + else if (c == 'k' || c == GRUB_TERM_UP) + { + int i = grub_gfxmenu_model_get_selected_index (model); + if (i > 0) + { + i--; + grub_gfxmenu_model_set_selected_index (model, i); + } + } + else if (c == '\r' || c == '\n' || c == GRUB_TERM_RIGHT) + { + int selected = grub_gfxmenu_model_get_selected_index (model); + int num_entries = grub_gfxmenu_model_get_num_entries (model); + if (selected >= 0 && selected < num_entries) + { + grub_menu_entry_t entry = + grub_gfxmenu_model_get_entry (model, selected); + grub_gfxmenu_view_execute_entry (view, entry); + } + } + else if (c == 'c') + { + grub_gfxmenu_view_run_terminal (view); + if (grub_errno != GRUB_ERR_NONE) + *should_exit = 1; + } + else if (c == 't') + { + /* The write hook for 'menuviewer' will cause + * grub_menu_viewer_should_return to return nonzero. */ + switch_to_text_menu (); + *should_exit = 1; + } + else if (c == '1') + { + grub_gfxmenu_view_load_theme (view, + "/boot/grub/themes/proto/theme.txt"); + } + else if (c == '2') + { + grub_gfxmenu_view_load_theme (view, + "/boot/grub/themes/winter/theme.txt"); + } + else if (nested && c == GRUB_TERM_ESC) + { + *should_exit = 1; + } +} + +static void +handle_key_events (grub_gfxmenu_model_t model, + grub_gfxmenu_view_t view, + int nested, + int *should_exit) +{ + while (!*should_exit && grub_checkkey () != -1) + { + int key = grub_getkey (); + int c = GRUB_TERM_ASCII_CHAR (key); + process_key_press (c, model, view, nested, should_exit); + } +} + +static grub_err_t +show_menu (grub_menu_t menu, int nested) +{ + grub_gfxmenu_model_t model; + + model = grub_gfxmenu_model_new (menu); + if (! model) + { + grub_print_error (); + grub_printf ("Initializing menu data for graphical menu failed;\n" + "falling back to terminal based menu.\n"); + grub_wait_after_message (); + switch_to_text_menu (); + return grub_errno; + } + + grub_gfxmenu_view_t view; + + /* Create the view. */ + const char *theme_path = grub_env_get ("theme"); + if (! theme_path) + theme_path = "/boot/grub/themes/proto/theme.txt"; + + view = grub_gfxmenu_view_new (theme_path, model); + if (! view) + { + grub_print_error (); + grub_printf ("Starting graphical menu failed;\n" + "falling back to terminal based menu.\n"); + grub_wait_after_message (); + grub_gfxmenu_model_destroy (model); + switch_to_text_menu (); + return grub_errno; + } + + /* Initially select the default menu entry. */ + int default_index = grub_menu_get_default_entry_index (menu); + grub_gfxmenu_model_set_selected_index (model, default_index); + + /* Start the timer to execute the default entry. */ + grub_gfxmenu_model_set_timeout (model); + + /* Main event loop. */ + int exit_requested = 0; + while (!exit_requested && !grub_menu_viewer_should_return ()) + { + if (grub_gfxmenu_model_timeout_expired (model)) + { + grub_gfxmenu_model_clear_timeout (model); + int i = grub_gfxmenu_model_get_selected_index (model); + grub_menu_entry_t e = grub_gfxmenu_model_get_entry (model, i); + grub_gfxmenu_view_execute_with_fallback (view, e); + continue; + } + + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + handle_key_events (model, view, nested, &exit_requested); + } + + grub_gfxmenu_view_destroy (view); + grub_gfxmenu_model_destroy (model); + + return grub_errno; +} + +static grub_err_t +grub_cmd_gfxmenu (struct grub_arg_list *state __attribute__ ((unused)), + int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + grub_menu_t menu = grub_env_get_data_slot ("menu"); + if (!menu) + return grub_error (GRUB_ERR_MENU, "No menu context"); + + return show_menu (menu, 1); +} + +static struct grub_menu_viewer menu_viewer = +{ + .name = "gfxmenu", + .show_menu = show_menu +}; + +GRUB_MOD_INIT (gfxmenu) +{ + (void) mod; /* To stop warning. */ + grub_menu_viewer_register (&menu_viewer); + grub_register_command ("gfxmenu", + grub_cmd_gfxmenu, GRUB_COMMAND_FLAG_BOTH, + "gfxmenu", "Show graphical menu interface", 0); +} + +GRUB_MOD_FINI (gfxmenu) +{ + grub_unregister_command ("gfxmenu"); +} === added file 'gfxmenu/gui_box.c' --- gfxmenu/gui_box.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gui_box.c 2008-08-12 02:48:48 +0000 @@ -0,0 +1,367 @@ +/* gui_box.c - GUI container that stack components. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +struct component_node +{ + grub_gui_component_t component; + struct component_node *next; + struct component_node *prev; +}; + +typedef struct grub_gui_box *grub_gui_box_t; + +typedef void (*layout_func_t) (grub_gui_box_t self, int modify_layout, + int *width, int *height); + +struct grub_gui_box +{ + struct grub_gui_container_ops *container; + + grub_gui_container_t parent; + grub_video_rect_t bounds; + char *id; + int preferred_width; + int preferred_height; + + /* Doubly linked list of components with dummy head & tail nodes. */ + struct component_node chead; + struct component_node ctail; + + /* The layout function: differs for vertical and horizontal boxes. */ + layout_func_t layout_func; +}; + +static void +box_destroy (void *vself) +{ + grub_gui_box_t self = vself; + struct component_node *cur; + struct component_node *next; + for (cur = self->chead.next; cur != &self->ctail; cur = next) + { + /* Copy the 'next' pointer, since we need it for the next iteration, + and we're going to free the memory it is stored in. */ + next = cur->next; + /* Destroy the child component. */ + cur->component->ops->destroy (cur->component); + /* Free the linked list node. */ + grub_free (cur); + } + grub_free (self); +} + +static const char * +box_get_id (void *vself) +{ + grub_gui_box_t self = vself; + return self->id; +} + +static int +box_is_instance (void *vself __attribute__((unused)), const char *type) +{ + return (grub_strcmp (type, "component") == 0 + || grub_strcmp (type, "container") == 0); +} + +static void +layout_horizontally (grub_gui_box_t self, int modify_layout, + int *width, int *height) +{ + /* Start at the left (chead) and set the x coordinates as we go right. */ + /* All components have their width set to the box's width. */ + + struct component_node *cur; + int x = 0; + if (height) + *height = 0; + for (cur = self->chead.next; cur != &self->ctail; cur = cur->next) + { + grub_gui_component_t c = cur->component; + grub_video_rect_t r; + + c->ops->get_preferred_size (c, &r.width, &r.height); + + /* Check and possibly update the maximum width, if non-null. */ + if (height && r.height > *height) + *height = r.height; + + /* Set the component's bounds, if the flag is set. */ + if (modify_layout) + { + r.x = x; + r.y = 0; + /* Width comes from the component's preferred size. */ + r.height = self->bounds.height; + c->ops->set_bounds (c, &r); + } + + x += r.width; + } + + /* Return the sum of the children's preferred widths. */ + if (width) + *width = x; +} + +static void +layout_vertically (grub_gui_box_t self, int modify_layout, + int *width, int *height) +{ + /* Start at the top (chead) and set the y coordinates as we go down. */ + /* All components have their width set to the vbox's width. */ + + struct component_node *cur; + int y = 0; + if (width) + *width = 0; + for (cur = self->chead.next; cur != &self->ctail; cur = cur->next) + { + grub_gui_component_t c = cur->component; + grub_video_rect_t r; + + c->ops->get_preferred_size (c, &r.width, &r.height); + + /* Check and possibly update the maximum width, if non-null. */ + if (width && r.width > *width) + *width = r.width; + + /* Set the component's bounds, if the flag is set. */ + if (modify_layout) + { + r.x = 0; + r.y = y; + r.width = self->bounds.width; + /* Height comes from the component's preferred size. */ + c->ops->set_bounds (c, &r); + } + + y += r.height; + } + + /* Return the sum of the children's preferred heights. */ + if (height) + *height = y; +} + +static void +box_paint (void *vself) +{ + grub_gui_box_t self = vself; + struct component_node *cur; + grub_video_rect_t vpsave; + + grub_gui_set_viewport (&self->bounds, &vpsave); + for (cur = self->chead.next; cur != &self->ctail; cur = cur->next) + { + grub_gui_component_t comp = cur->component; + comp->ops->paint (comp); + } + grub_gui_restore_viewport (&vpsave); +} + +static void +box_set_parent (void *vself, grub_gui_container_t parent) +{ + grub_gui_box_t self = vself; + self->parent = parent; +} + +static grub_gui_container_t +box_get_parent (void *vself) +{ + grub_gui_box_t self = vself; + return self->parent; +} + +static void +box_set_bounds (void *vself, const grub_video_rect_t *bounds) +{ + grub_gui_box_t self = vself; + self->bounds = *bounds; + self->layout_func (self, 1, 0, 0); /* Relayout the children. */ +} + +static void +box_get_bounds (void *vself, grub_video_rect_t *bounds) +{ + grub_gui_box_t self = vself; + *bounds = self->bounds; +} + +/* The box's preferred size is based on the preferred sizes + of its children. */ +static void +box_get_preferred_size (void *vself, int *width, int *height) +{ + grub_gui_box_t self = vself; + self->layout_func (self, 0, width, height); /* Just calculate the size. */ + + /* Allow preferred dimensions to override the computed dimensions. */ + if (self->preferred_width >= 0) + *width = self->preferred_width; + if (self->preferred_height >= 0) + *height = self->preferred_height; +} + +static void +box_set_property (void *vself, const char *name, const char *value) +{ + grub_gui_box_t self = vself; + if (grub_strcmp (name, "id") == 0) + { + grub_free (self->id); + if (value) + self->id = grub_strdup (value); + else + self->id = 0; + } + else if (grub_strcmp (name, "preferred_size") == 0) + { + int w; + int h; + if (grub_gui_parse_2_tuple (value, &w, &h)) + { + self->preferred_width = w; + self->preferred_height = h; + } + } +} + +static void +box_add (void *vself, grub_gui_component_t comp) +{ + grub_gui_box_t self = vself; + struct component_node *node; + node = grub_malloc (sizeof (*node)); + if (! node) + return; /* Note: probably should handle the error. */ + node->component = comp; + /* Insert the node before the tail. */ + node->prev = self->ctail.prev; + node->prev->next = node; + node->next = &self->ctail; + node->next->prev = node; + + comp->ops->set_parent (comp, (grub_gui_container_t) self); + self->layout_func (self, 1, 0, 0); /* Relayout the children. */ +} + +static void +box_remove (void *vself, grub_gui_component_t comp) +{ + grub_gui_box_t self = vself; + struct component_node *cur; + for (cur = self->chead.next; cur != &self->ctail; cur = cur->next) + { + if (cur->component == comp) + { + /* Unlink 'cur' from the list. */ + cur->prev->next = cur->next; + cur->next->prev = cur->prev; + /* Free the node's memory (but don't destroy the component). */ + grub_free (cur); + /* Must not loop again, since 'cur' would be dereferenced! */ + return; + } + } +} + +static void +box_iterate_children (void *vself, + grub_gui_component_callback cb, void *userdata) +{ + grub_gui_box_t self = vself; + struct component_node *cur; + for (cur = self->chead.next; cur != &self->ctail; cur = cur->next) + cb (cur->component, userdata); +} + +static struct grub_gui_container_ops box_ops = +{ + .component = + { + .destroy = box_destroy, + .get_id = box_get_id, + .is_instance = box_is_instance, + .paint = box_paint, + .set_parent = box_set_parent, + .get_parent = box_get_parent, + .set_bounds = box_set_bounds, + .get_bounds = box_get_bounds, + .get_preferred_size = box_get_preferred_size, + .set_property = box_set_property + }, + .add = box_add, + .remove = box_remove, + .iterate_children = box_iterate_children +}; + +/* Box constructor. Specify the appropriate layout function to create + a horizontal or vertical stacking box. */ +static grub_gui_box_t +box_new (layout_func_t layout_func) +{ + grub_gui_box_t box; + box = grub_malloc (sizeof (*box)); + if (! box) + return 0; + box->container = &box_ops; + box->parent = 0; + box->bounds.x = 0; + box->bounds.y = 0; + box->bounds.width = 0; + box->bounds.height = 0; + box->id = 0; + box->preferred_width = -1; + box->preferred_height = -1; + box->chead.component = 0; + box->chead.prev = 0; + box->chead.next = &box->ctail; + box->ctail.component = 0; + box->ctail.prev = &box->chead; + box->ctail.next = 0; + box->layout_func = layout_func; + return box; +} + +/* Create a new container that stacks its child components horizontally, + from left to right. Each child get a width corresponding to its + preferred width. The height of each child is set the maximum of the + preferred heights of all children. */ +grub_gui_container_t +grub_gui_hbox_new (void) +{ + return (grub_gui_container_t) box_new (layout_horizontally); +} + +/* Create a new container that stacks its child components verticallyj, + from top to bottom. Each child get a height corresponding to its + preferred height. The width of each child is set the maximum of the + preferred widths of all children. */ +grub_gui_container_t +grub_gui_vbox_new (void) +{ + return (grub_gui_container_t) box_new (layout_vertically); +} === added file 'gfxmenu/gui_canvas.c' --- gfxmenu/gui_canvas.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gui_canvas.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,247 @@ +/* gui_canvas.c - GUI container allowing manually placed components. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +// TODO Add layering so that components can be properly overlaid. + +struct component_node +{ + grub_gui_component_t component; + struct component_node *next; +}; + +struct grub_gui_canvas +{ + struct grub_gui_container_ops *container; + + grub_gui_container_t parent; + grub_video_rect_t bounds; + char *id; + int preferred_width; + int preferred_height; + /* Component list (dummy head node). */ + struct component_node components; +}; + +typedef struct grub_gui_canvas *grub_gui_canvas_t; + +static void +canvas_destroy (void *vself) +{ + grub_gui_canvas_t self = vself; + struct component_node *cur; + struct component_node *next; + for (cur = self->components.next; cur; cur = next) + { + /* Copy the 'next' pointer, since we need it for the next iteration, + and we're going to free the memory it is stored in. */ + next = cur->next; + /* Destroy the child component. */ + cur->component->ops->destroy (cur->component); + /* Free the linked list node. */ + grub_free (cur); + } + grub_free (self); +} + +static const char * +canvas_get_id (void *vself) +{ + grub_gui_canvas_t self = vself; + return self->id; +} + +static int +canvas_is_instance (void *vself __attribute__((unused)), const char *type) +{ + return (grub_strcmp (type, "component") == 0 + || grub_strcmp (type, "container") == 0); +} + +static void +canvas_paint (void *vself) +{ + grub_gui_canvas_t self = vself; + struct component_node *cur; + grub_video_rect_t vpsave; + + grub_gui_set_viewport (&self->bounds, &vpsave); + for (cur = self->components.next; cur; cur = cur->next) + { + grub_gui_component_t comp = cur->component; + comp->ops->paint (comp); + } + grub_gui_restore_viewport (&vpsave); +} + +static void +canvas_set_parent (void *vself, grub_gui_container_t parent) +{ + grub_gui_canvas_t self = vself; + self->parent = parent; +} + +static grub_gui_container_t +canvas_get_parent (void *vself) +{ + grub_gui_canvas_t self = vself; + return self->parent; +} + +static void +canvas_set_bounds (void *vself, const grub_video_rect_t *bounds) +{ + grub_gui_canvas_t self = vself; + self->bounds = *bounds; +} + +static void +canvas_get_bounds (void *vself, grub_video_rect_t *bounds) +{ + grub_gui_canvas_t self = vself; + *bounds = self->bounds; +} + +static void +canvas_get_preferred_size (void *vself, int *width, int *height) +{ + grub_gui_canvas_t self = vself; + *width = 0; + *height = 0; + + /* Allow preferred dimensions to override the empty dimensions. */ + if (self->preferred_width >= 0) + *width = self->preferred_width; + if (self->preferred_height >= 0) + *height = self->preferred_height; +} + +static void +canvas_set_property (void *vself, const char *name, const char *value) +{ + grub_gui_canvas_t self = vself; + if (grub_strcmp (name, "id") == 0) + { + grub_free (self->id); + if (value) + self->id = grub_strdup (value); + else + self->id = 0; + } + else if (grub_strcmp (name, "preferred_size") == 0) + { + int w; + int h; + if (grub_gui_parse_2_tuple (value, &w, &h)) + { + self->preferred_width = w; + self->preferred_height = h; + } + } +} + +static void +canvas_add (void *vself, grub_gui_component_t comp) +{ + grub_gui_canvas_t self = vself; + struct component_node *node; + node = grub_malloc (sizeof (*node)); + if (! node) + return; /* Note: probably should handle the error. */ + node->component = comp; + node->next = self->components.next; + self->components.next = node; + comp->ops->set_parent (comp, (grub_gui_container_t) self); +} + +static void +canvas_remove (void *vself, grub_gui_component_t comp) +{ + grub_gui_canvas_t self = vself; + struct component_node *cur; + struct component_node *prev; + prev = &self->components; + for (cur = self->components.next; cur; prev = cur, cur = cur->next) + { + if (cur->component == comp) + { + /* Unlink 'cur' from the list. */ + prev->next = cur->next; + /* Free the node's memory (but don't destroy the component). */ + grub_free (cur); + /* Must not loop again, since 'cur' would be dereferenced! */ + return; + } + } +} + +static void +canvas_iterate_children (void *vself, + grub_gui_component_callback cb, void *userdata) +{ + grub_gui_canvas_t self = vself; + struct component_node *cur; + for (cur = self->components.next; cur; cur = cur->next) + cb (cur->component, userdata); +} + +static struct grub_gui_container_ops canvas_ops = +{ + .component = + { + .destroy = canvas_destroy, + .get_id = canvas_get_id, + .is_instance = canvas_is_instance, + .paint = canvas_paint, + .set_parent = canvas_set_parent, + .get_parent = canvas_get_parent, + .set_bounds = canvas_set_bounds, + .get_bounds = canvas_get_bounds, + .get_preferred_size = canvas_get_preferred_size, + .set_property = canvas_set_property + }, + .add = canvas_add, + .remove = canvas_remove, + .iterate_children = canvas_iterate_children +}; + +grub_gui_container_t +grub_gui_canvas_new (void) +{ + grub_gui_canvas_t canvas; + canvas = grub_malloc (sizeof (*canvas)); + if (! canvas) + return 0; + canvas->container = &canvas_ops; + canvas->parent = 0; + canvas->bounds.x = 0; + canvas->bounds.y = 0; + canvas->bounds.width = 0; + canvas->bounds.height = 0; + canvas->id = 0; + canvas->preferred_width = -1; + canvas->preferred_height = -1; + canvas->components.component = 0; + canvas->components.next = 0; + return (grub_gui_container_t) canvas; +} === added file 'gfxmenu/gui_image.c' --- gfxmenu/gui_image.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gui_image.c 2008-08-10 15:14:06 +0000 @@ -0,0 +1,262 @@ +/* gui_image.c - GUI component to display an image. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +struct grub_gui_image +{ + struct grub_gui_component_ops *image; + + grub_gui_container_t parent; + grub_video_rect_t bounds; + char *id; + int preferred_width; + int preferred_height; + struct grub_video_bitmap *raw_bitmap; + struct grub_video_bitmap *bitmap; +}; + +typedef struct grub_gui_image *grub_gui_image_t; + +static void +image_destroy (void *vself) +{ + grub_gui_image_t self = vself; + + /* Free the scaled bitmap, unless it's a reference to the raw bitmap. */ + if (self->bitmap && (self->bitmap != self->raw_bitmap)) + grub_video_bitmap_destroy (self->bitmap); + if (self->raw_bitmap) + grub_video_bitmap_destroy (self->raw_bitmap); + + grub_free (self); +} + +static const char * +image_get_id (void *vself) +{ + grub_gui_image_t self = vself; + return self->id; +} + +static int +image_is_instance (void *vself __attribute__((unused)), const char *type) +{ + return grub_strcmp (type, "component") == 0; +} + +static void +image_paint (void *vself) +{ + grub_gui_image_t self = vself; + if (! self->bitmap) + return; + grub_video_rect_t vpsave; + grub_gui_set_viewport (&self->bounds, &vpsave); + grub_video_blit_bitmap (self->bitmap, GRUB_VIDEO_BLIT_BLEND, + 0, 0, 0, 0, + grub_video_bitmap_get_width (self->bitmap), + grub_video_bitmap_get_height (self->bitmap)); + grub_gui_restore_viewport (&vpsave); +} + +static void +image_set_parent (void *vself, grub_gui_container_t parent) +{ + grub_gui_image_t self = vself; + self->parent = parent; +} + +static grub_gui_container_t +image_get_parent (void *vself) +{ + grub_gui_image_t self = vself; + return self->parent; +} + +static void +rescale_image (grub_gui_image_t self) +{ + if (! self->raw_bitmap) + { + if (self->bitmap) + { + grub_video_bitmap_destroy (self->bitmap); + self->bitmap = 0; + } + return; + } + + unsigned width = self->bounds.width; + unsigned height = self->bounds.height; + + if (self->bitmap + && (grub_video_bitmap_get_width (self->bitmap) == width) + && (grub_video_bitmap_get_height (self->bitmap) == height)) + return; /* Nothing to do; already the right size. */ + + /* Free any old scaled bitmap, + unless it's a reference to the raw bitmap. */ + if (self->bitmap && (self->bitmap != self->raw_bitmap)) + grub_video_bitmap_destroy (self->bitmap); + + /* Create a scaled bitmap, unless the requested size is the same + as the raw size -- in that case a reference is made. */ + if (grub_video_bitmap_get_width (self->raw_bitmap) == width + && grub_video_bitmap_get_height (self->raw_bitmap) == height) + { + self->bitmap = self->raw_bitmap; + return; + } + + /* Create the scaled bitmap. */ + grub_video_bitmap_create_scaled (&self->bitmap, + width, + height, + self->raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_errno = GRUB_ERR_NONE; /* Catch any errors. */ +} + +static void +image_set_bounds (void *vself, const grub_video_rect_t *bounds) +{ + grub_gui_image_t self = vself; + self->bounds = *bounds; + rescale_image (self); +} + +static void +image_get_bounds (void *vself, grub_video_rect_t *bounds) +{ + grub_gui_image_t self = vself; + *bounds = self->bounds; +} + +static void +image_get_preferred_size (void *vself, int *width, int *height) +{ + grub_gui_image_t self = vself; + + if (self->raw_bitmap) + { + *width = grub_video_bitmap_get_width (self->raw_bitmap); + *height = grub_video_bitmap_get_height (self->raw_bitmap); + } + else + { + *width = 0; + *height = 0; + } + + /* Allow preferred dimensions to override the image dimensions. */ + if (self->preferred_width >= 0) + *width = self->preferred_width; + if (self->preferred_height >= 0) + *height = self->preferred_height; +} + +static void +load_image (grub_gui_image_t self, const char *path) +{ + struct grub_video_bitmap *bitmap; + grub_errno = GRUB_ERR_NONE; + if (grub_video_bitmap_load (&bitmap, path) != GRUB_ERR_NONE) + { + /* We should log the error somehow. */ + grub_errno = GRUB_ERR_NONE; + return; + } + if (self->bitmap && (self->bitmap != self->raw_bitmap)) + grub_video_bitmap_destroy (self->bitmap); + if (self->raw_bitmap) + grub_video_bitmap_destroy (self->raw_bitmap); + + self->raw_bitmap = bitmap; + rescale_image (self); +} + +static void +image_set_property (void *vself, const char *name, const char *value) +{ + grub_gui_image_t self = vself; + if (grub_strcmp (name, "file") == 0) + { + load_image (self, value); + } + else if (grub_strcmp (name, "preferred_size") == 0) + { + int w; + int h; + if (grub_gui_parse_2_tuple (value, &w, &h)) + { + self->preferred_width = w; + self->preferred_height = h; + } + } + else if (grub_strcmp (name, "id") == 0) + { + grub_free (self->id); + if (value) + self->id = grub_strdup (value); + else + self->id = 0; + } +} + +static struct grub_gui_component_ops image_ops = +{ + .destroy = image_destroy, + .get_id = image_get_id, + .is_instance = image_is_instance, + .paint = image_paint, + .set_parent = image_set_parent, + .get_parent = image_get_parent, + .set_bounds = image_set_bounds, + .get_bounds = image_get_bounds, + .get_preferred_size = image_get_preferred_size, + .set_property = image_set_property +}; + +grub_gui_component_t +grub_gui_image_new (void) +{ + grub_gui_image_t image; + image = grub_malloc (sizeof (*image)); + if (! image) + return 0; + image->image = &image_ops; + image->parent = 0; + image->bounds.x = 0; + image->bounds.y = 0; + image->bounds.width = 0; + image->bounds.height = 0; + image->id = 0; + image->preferred_width = -1; + image->preferred_height = -1; + image->raw_bitmap = 0; + image->bitmap = 0; + return (grub_gui_component_t) image; +} + === added file 'gfxmenu/gui_label.c' --- gfxmenu/gui_label.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gui_label.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,247 @@ +/* gui_label.c - GUI component to display a line of text. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +static const char *align_options[] = +{ + "left", + "center", + "right", + 0 +}; + +enum align_mode { + align_left, + align_center, + align_right +}; + +struct grub_gui_label +{ + struct grub_gui_component_ops *label; + + grub_gui_container_t parent; + grub_video_rect_t bounds; + char *id; + int preferred_width; + int preferred_height; + int visible; + char *text; + grub_font_t font; + grub_gui_color_t color; + enum align_mode align; +}; + +typedef struct grub_gui_label *grub_gui_label_t; + +static void +label_destroy (void *vself) +{ + grub_gui_label_t self = vself; + grub_free (self->text); + grub_free (self); +} + +static const char * +label_get_id (void *vself) +{ + grub_gui_label_t self = vself; + return self->id; +} + +static int +label_is_instance (void *vself __attribute__((unused)), const char *type) +{ + return grub_strcmp (type, "component") == 0; +} + +static void +label_paint (void *vself) +{ + grub_gui_label_t self = vself; + + if (! self->visible) + return; + + /* Calculate the starting x coordinate. */ + int left_x; + if (self->align == align_left) + left_x = 0; + else if (self->align == align_center) + left_x = ((self->bounds.width + - grub_font_get_string_width (self->font, self->text)) + ) / 2; + else if (self->align == align_right) + left_x = (self->bounds.width + - grub_font_get_string_width (self->font, self->text)); + else + return; /* Invalid alignment. */ + + grub_video_rect_t vpsave; + grub_gui_set_viewport (&self->bounds, &vpsave); + grub_video_draw_string (self->text, + self->font, + grub_gui_map_color (self->color), + left_x, + grub_font_get_ascent (self->font)); + grub_gui_restore_viewport (&vpsave); +} + +static void +label_set_parent (void *vself, grub_gui_container_t parent) +{ + grub_gui_label_t self = vself; + self->parent = parent; +} + +static grub_gui_container_t +label_get_parent (void *vself) +{ + grub_gui_label_t self = vself; + return self->parent; +} + +static void +label_set_bounds (void *vself, const grub_video_rect_t *bounds) +{ + grub_gui_label_t self = vself; + self->bounds = *bounds; +} + +static void +label_get_bounds (void *vself, grub_video_rect_t *bounds) +{ + grub_gui_label_t self = vself; + *bounds = self->bounds; +} + +static void +label_get_preferred_size (void *vself, int *width, int *height) +{ + grub_gui_label_t self = vself; + *width = grub_font_get_string_width (self->font, self->text); + *height = (grub_font_get_ascent (self->font) + + grub_font_get_descent (self->font)); + + /* Allow preferred dimensions to override the computed dimensions. */ + if (self->preferred_width >= 0) + *width = self->preferred_width; + if (self->preferred_height >= 0) + *height = self->preferred_height; +} + +static void +label_set_property (void *vself, const char *name, const char *value) +{ + grub_gui_label_t self = vself; + if (grub_strcmp (name, "text") == 0) + { + grub_free (self->text); + if (! value) + value = ""; + self->text = grub_strdup (value); + } + else if (grub_strcmp (name, "font") == 0) + { + self->font = grub_font_get (value); + } + else if (grub_strcmp (name, "color") == 0) + { + self->color = grub_gui_parse_color (value); + } + else if (grub_strcmp (name, "align") == 0) + { + int i; + for (i = 0; align_options[i]; i++) + { + if (grub_strcmp (align_options[i], value) == 0) + { + self->align = i; /* Set the alignment mode. */ + break; + } + } + } + else if (grub_strcmp (name, "visible") == 0) + { + self->visible = grub_strcmp (value, "false") != 0; + } + else if (grub_strcmp (name, "preferred_size") == 0) + { + int w; + int h; + if (grub_gui_parse_2_tuple (value, &w, &h)) + { + self->preferred_width = w; + self->preferred_height = h; + } + } + else if (grub_strcmp (name, "id") == 0) + { + grub_free (self->id); + if (value) + self->id = grub_strdup (value); + else + self->id = 0; + } +} + +static struct grub_gui_component_ops label_ops = +{ + .destroy = label_destroy, + .get_id = label_get_id, + .is_instance = label_is_instance, + .paint = label_paint, + .set_parent = label_set_parent, + .get_parent = label_get_parent, + .set_bounds = label_set_bounds, + .get_bounds = label_get_bounds, + .get_preferred_size = label_get_preferred_size, + .set_property = label_set_property +}; + +grub_gui_component_t +grub_gui_label_new (void) +{ + grub_gui_label_t label; + label = grub_malloc (sizeof (*label)); + if (! label) + return 0; + label->label = &label_ops; + label->parent = 0; + label->bounds.x = 0; + label->bounds.y = 0; + label->bounds.width = 0; + label->bounds.height = 0; + label->id = 0; + label->preferred_width = -1; + label->preferred_height = -1; + label->visible = 1; + label->text = grub_strdup (""); + label->font = grub_font_get ("Helvetica 10"); + label->color.red = 0; + label->color.green = 0; + label->color.blue = 0; + label->color.alpha = 255; + label->align = align_left; + return (grub_gui_component_t) label; +} === added file 'gfxmenu/gui_progress_bar.c' --- gfxmenu/gui_progress_bar.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gui_progress_bar.c 2008-08-11 14:36:33 +0000 @@ -0,0 +1,278 @@ +/* gui_progress_bar.c - GUI progress bar component. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include + +struct grub_gui_progress_bar +{ + struct grub_gui_component_ops *progress_bar; + + grub_gui_container_t parent; + grub_video_rect_t bounds; + char *id; + int preferred_width; + int preferred_height; + int visible; + int start; + int end; + int value; + int show_text; + char *text; + grub_font_t font; + grub_gui_color_t text_color; + grub_gui_color_t border_color; + grub_gui_color_t bg_color; + grub_gui_color_t fg_color; +}; + +typedef struct grub_gui_progress_bar *grub_gui_progress_bar_t; + +static void +progress_bar_destroy (void *vself) +{ + grub_gui_progress_bar_t self = vself; + grub_free (self); +} + +static const char * +progress_bar_get_id (void *vself) +{ + grub_gui_progress_bar_t self = vself; + return self->id; +} + +static int +progress_bar_is_instance (void *vself __attribute__((unused)), const char *type) +{ + return grub_strcmp (type, "component") == 0; +} + +static void +progress_bar_paint (void *vself) +{ + grub_gui_progress_bar_t self = vself; + if (! self->visible) + return; + + grub_video_rect_t vpsave; + grub_gui_set_viewport (&self->bounds, &vpsave); + + /* Set the progress bar's frame. */ + grub_video_rect_t f; + f.x = 1; + f.y = 1; + f.width = self->bounds.width - 2; + f.height = self->bounds.height - 2; + + /* Border. */ + grub_video_fill_rect (grub_gui_map_color (self->border_color), + f.x - 1, f.y - 1, + f.width + 2, f.height + 2); + + /* Bar background. */ + int barwidth = (f.width + * (self->value - self->start) + / (self->end - self->start)); + grub_video_fill_rect (grub_gui_map_color (self->bg_color), + f.x + barwidth, f.y, + f.width - barwidth, f.height); + + /* Bar foreground. */ + grub_video_fill_rect (grub_gui_map_color (self->fg_color), + f.x, f.y, + barwidth, f.height); + + const char *text = self->text; + if (text && self->show_text) + { + grub_font_t font = self->font; + grub_video_color_t text_color = grub_gui_map_color (self->text_color); + + /* Center the text. */ + int text_width = grub_font_get_string_width (font, text); + int x = f.x + (f.width - text_width) / 2; + int y = (f.y + (f.height - grub_font_get_descent (font)) / 2 + + grub_font_get_ascent (font) / 2); + grub_video_draw_string (text, font, text_color, x, y); + } + + grub_gui_restore_viewport (&vpsave); +} + +static void +progress_bar_set_parent (void *vself, grub_gui_container_t parent) +{ + grub_gui_progress_bar_t self = vself; + self->parent = parent; +} + +static grub_gui_container_t +progress_bar_get_parent (void *vself) +{ + grub_gui_progress_bar_t self = vself; + return self->parent; +} + +static void +progress_bar_set_bounds (void *vself, const grub_video_rect_t *bounds) +{ + grub_gui_progress_bar_t self = vself; + self->bounds = *bounds; +} + +static void +progress_bar_get_bounds (void *vself, grub_video_rect_t *bounds) +{ + grub_gui_progress_bar_t self = vself; + *bounds = self->bounds; +} + +static void +progress_bar_get_preferred_size (void *vself, int *width, int *height) +{ + grub_gui_progress_bar_t self = vself; + + *width = 200; + *height = 28; + + /* Allow preferred dimensions to override the progress_bar dimensions. */ + if (self->preferred_width >= 0) + *width = self->preferred_width; + if (self->preferred_height >= 0) + *height = self->preferred_height; +} + +static void +progress_bar_set_property (void *vself, const char *name, const char *value) +{ + grub_gui_progress_bar_t self = vself; + if (grub_strcmp (name, "value") == 0) + { + self->value = grub_strtol (value, 0, 10); + } + else if (grub_strcmp (name, "start") == 0) + { + self->start = grub_strtol (value, 0, 10); + } + else if (grub_strcmp (name, "end") == 0) + { + self->end = grub_strtol (value, 0, 10); + } + else if (grub_strcmp (name, "text") == 0) + { + grub_free (self->text); + if (! value) + value = ""; + self->text = grub_strdup (value); + } + else if (grub_strcmp (name, "font") == 0) + { + self->font = grub_font_get (value); + } + else if (grub_strcmp (name, "text_color") == 0) + { + self->text_color = grub_gui_parse_color (value); + } + else if (grub_strcmp (name, "border_color") == 0) + { + self->border_color = grub_gui_parse_color (value); + } + else if (grub_strcmp (name, "bg_color") == 0) + { + self->bg_color = grub_gui_parse_color (value); + } + else if (grub_strcmp (name, "fg_color") == 0) + { + self->fg_color = grub_gui_parse_color (value); + } + else if (grub_strcmp (name, "preferred_size") == 0) + { + int w; + int h; + if (grub_gui_parse_2_tuple (value, &w, &h)) + { + self->preferred_width = w; + self->preferred_height = h; + } + } + else if (grub_strcmp (name, "visible") == 0) + { + self->visible = grub_strcmp (value, "false") != 0; + } + else if (grub_strcmp (name, "show_text") == 0) + { + self->show_text = grub_strcmp (value, "false") != 0; + } + else if (grub_strcmp (name, "id") == 0) + { + grub_free (self->id); + if (value) + self->id = grub_strdup (value); + else + self->id = 0; + } +} + +static struct grub_gui_component_ops progress_bar_ops = +{ + .destroy = progress_bar_destroy, + .get_id = progress_bar_get_id, + .is_instance = progress_bar_is_instance, + .paint = progress_bar_paint, + .set_parent = progress_bar_set_parent, + .get_parent = progress_bar_get_parent, + .set_bounds = progress_bar_set_bounds, + .get_bounds = progress_bar_get_bounds, + .get_preferred_size = progress_bar_get_preferred_size, + .set_property = progress_bar_set_property +}; + +grub_gui_component_t +grub_gui_progress_bar_new (void) +{ + grub_gui_progress_bar_t self; + self = grub_malloc (sizeof (*self)); + if (! self) + return 0; + self->progress_bar = &progress_bar_ops; + self->parent = 0; + self->bounds.x = 0; + self->bounds.y = 0; + self->bounds.width = 0; + self->bounds.height = 0; + self->id = 0; + self->preferred_width = -1; + self->preferred_height = -1; + self->visible = 1; + self->show_text = 1; + self->text = grub_strdup (""); + self->font = grub_font_get ("Helvetica 10"); + grub_gui_color_t black = { .red = 0, .green = 0, .blue = 0, .alpha = 255 }; + grub_gui_color_t gray = { .red = 128, .green = 128, .blue = 128, .alpha = 255 }; + grub_gui_color_t lightgray = { .red = 200, .green = 200, .blue = 200, .alpha = 255 }; + self->text_color = black; + self->border_color = black; + self->bg_color = gray; + self->fg_color = lightgray; + return (grub_gui_component_t) self; +} === added file 'gfxmenu/gui_util.c' --- gfxmenu/gui_util.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/gui_util.c 2008-08-11 14:36:33 +0000 @@ -0,0 +1,64 @@ +/* gui_util.c - GUI utility functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +struct find_by_id_state +{ + const char *match_id; + grub_gui_component_callback match_callback; + void *match_userdata; +}; + +static +void find_by_id_recursively (grub_gui_component_t component, void *userdata) +{ + struct find_by_id_state *state; + const char *id; + + state = (struct find_by_id_state *) userdata; + id = component->ops->get_id (component); + if (id && grub_strcmp (id, state->match_id) == 0) + state->match_callback (component, state->match_userdata); + + if (component->ops->is_instance (component, "container")) + { + grub_gui_container_t container; + container = (grub_gui_container_t) component; + container->ops->iterate_children (container, + find_by_id_recursively, + state); + } +} + +void +grub_gui_find_by_id (grub_gui_component_t root, + const char *id, + grub_gui_component_callback cb, + void *userdata) +{ + struct find_by_id_state state; + state.match_id = id; + state.match_callback = cb; + state.match_userdata = userdata; + find_by_id_recursively (root, &state); +} === added file 'gfxmenu/model.c' --- gfxmenu/model.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/model.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,191 @@ +/* model.c - Graphical menu interface MVC model. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Model type definition. */ +struct grub_gfxmenu_model +{ + grub_menu_t menu; + int num_entries; + grub_menu_entry_t *entries; + int selected_entry_index; + int timeout_set; + grub_uint64_t timeout_start; + grub_uint64_t timeout_at; +}; + + +grub_gfxmenu_model_t +grub_gfxmenu_model_new (grub_menu_t menu) +{ + grub_gfxmenu_model_t model; + + model = grub_malloc (sizeof (*model)); + if (! model) + return 0; + + model->menu = menu; + model->num_entries = menu->size; + model->entries = 0; + model->selected_entry_index = 0; + model->timeout_set = 0; + model->timeout_at = 0; + if (model->num_entries > 0) + { + model->entries = grub_malloc (model->num_entries + * sizeof (*model->entries)); + if (! model->entries) + goto fail_and_free; + + int i; + grub_menu_entry_t cur; + for (i = 0, cur = menu->entry_list; + i < model->num_entries; + i++, cur = cur->next) + { + model->entries[i] = cur; + } + } + + return model; + +fail_and_free: + grub_free (model->entries); + grub_free (model); + return 0; +} + +void +grub_gfxmenu_model_destroy (grub_gfxmenu_model_t model) +{ + if (! model) + return; + + grub_free (model->entries); + model->entries = 0; + + grub_free (model); +} + +grub_menu_t +grub_gfxmenu_model_get_menu (grub_gfxmenu_model_t model) +{ + return model->menu; +} + +void +grub_gfxmenu_model_set_timeout (grub_gfxmenu_model_t model) +{ + int timeout_sec = grub_menu_get_timeout (); + if (timeout_sec >= 0) + { + model->timeout_start = grub_get_time_ms (); + model->timeout_at = model->timeout_start + timeout_sec * 1000; + model->timeout_set = 1; + } + else + { + model->timeout_set = 0; + } +} + +void +grub_gfxmenu_model_clear_timeout (grub_gfxmenu_model_t model) +{ + model->timeout_set = 0; + grub_menu_set_timeout (-1); +} + +int +grub_gfxmenu_model_get_timeout_ms (grub_gfxmenu_model_t model) +{ + if (!model->timeout_set) + return -1; + + return model->timeout_at - model->timeout_start; +} + +int +grub_gfxmenu_model_get_timeout_remaining_ms (grub_gfxmenu_model_t model) +{ + if (!model->timeout_set) + return -1; + + return model->timeout_at - grub_get_time_ms (); +} + +int +grub_gfxmenu_model_timeout_expired (grub_gfxmenu_model_t model) +{ + if (model->timeout_set + && grub_get_time_ms () >= model->timeout_at) + return 1; + + return 0; +} + +int +grub_gfxmenu_model_get_num_entries (grub_gfxmenu_model_t model) +{ + return model->num_entries; +} + +int +grub_gfxmenu_model_get_selected_index (grub_gfxmenu_model_t model) +{ + return model->selected_entry_index; +} + +void +grub_gfxmenu_model_set_selected_index (grub_gfxmenu_model_t model, int index) +{ + model->selected_entry_index = index; +} + +const char * +grub_gfxmenu_model_get_entry_title (grub_gfxmenu_model_t model, int index) +{ + if (index < 0 || index >= model->num_entries) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "invalid menu index"); + return 0; + } + + return model->entries[index]->title; +} + +grub_menu_entry_t +grub_gfxmenu_model_get_entry (grub_gfxmenu_model_t model, int index) +{ + if (index < 0 || index >= model->num_entries) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "invalid menu index"); + return 0; + } + + return model->entries[index]; +} === added file 'gfxmenu/stringutil.c' --- gfxmenu/stringutil.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/stringutil.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,343 @@ +/* stringutil.c - String utilities. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +/* Create a new NUL-terminated string on the heap as a substring of BUF. + The range of buf included is the half-open interval [START,END). + The index START is inclusive, END is exclusive. */ +char * +grub_new_substring (const char *buf, + grub_size_t start, grub_size_t end) +{ + if (end < start) + return 0; + grub_size_t len = end - start; + char *s = grub_malloc (len + 1); + if (! s) + return 0; + grub_memcpy (s, buf + start, len); + s[len] = '\0'; + return s; +} + +/* Eliminate "." and ".." path elements from PATH. A new heap-allocated + string is returned. */ +static char * +canonicalize_path (const char *path) +{ + int i; + const char *p; + char *newpath = 0; + + /* Count the path components in path. */ + int components = 1; + for (p = path; *p; p++) + if (*p == '/') + components++; + + char **path_array = grub_malloc (components * sizeof (*path_array)); + if (! path_array) + return 0; + + /* Initialize array elements to NULL pointers; in case once of the + allocations fails, the cleanup code can just call grub_free() for all + pointers in the array. */ + for (i = 0; i < components; i++) + path_array[i] = 0; + + /* Parse the path into path_array. */ + p = path; + for (i = 0; i < components && p; i++) + { + /* Find the end of the path element. */ + const char *end = grub_strchr (p, '/'); + if (!end) + end = p + grub_strlen (p); + + /* Copy the element. */ + path_array[i] = grub_new_substring (p, 0, end - p); + if (!path_array[i]) + goto cleanup; + + /* Advance p to point to the start of the next element, or NULL. */ + if (*end) + p = end + 1; + else + p = 0; + } + + /* Eliminate '.' and '..' elements from the path array. */ + int newpath_length = 0; + for (i = components - 1; i >= 0; --i) + { + if (! grub_strcmp (path_array[i], ".")) + { + grub_free (path_array[i]); + path_array[i] = 0; + } + else if (! grub_strcmp (path_array[i], "..") + && i > 0) + { + /* Delete the '..' and the prior path element. */ + grub_free (path_array[i]); + path_array[i] = 0; + --i; + grub_free (path_array[i]); + path_array[i] = 0; + } + else + { + newpath_length += grub_strlen (path_array[i]) + 1; + } + } + + /* Construct a new path string. */ + newpath = grub_malloc (newpath_length + 1); + if (! newpath) + goto cleanup; + + newpath[0] = '\0'; + char *newpath_end = newpath; + int first = 1; + for (i = 0; i < components; i++) + { + char *element = path_array[i]; + if (element) + { + /* For all components but the first, prefix with a slash. */ + if (! first) + newpath_end = grub_stpcpy (newpath_end, "/"); + newpath_end = grub_stpcpy (newpath_end, element); + first = 0; + } + } + +cleanup: + for (i = 0; i < components; i++) + grub_free (path_array[i]); + grub_free (path_array); + + return newpath; +} + +/* Return a new heap-allocated string representing to absolute path + to the file referred to by PATH. If PATH is an absolute path, then + the returned path is a copy of PATH. If PATH is a relative path, then + BASE is with PATH used to construct the absolute path. */ +char * +grub_resolve_relative_path (const char *base, const char *path) +{ + char *abspath; + char *canonpath; + char *p; + + /* If PATH is an absolute path, then just use it as is. */ + if (path[0] == '/' || path[0] == '(') + return canonicalize_path (path); + + abspath = grub_malloc (grub_strlen (base) + grub_strlen (path) + 1); + if (!abspath) + return 0; + + /* Concatenate BASE and PATH. + Note that BASE is expected to have a trailing slash. */ + p = grub_stpcpy (abspath, base); + grub_stpcpy (p, path); + + canonpath = canonicalize_path (abspath); + if (!canonpath) + return abspath; + + grub_free (abspath); + return canonpath; +} + +/* Get the path of the directory where the file at FILE_PATH is located. + FILE_PATH should refer to a file, not a directory. The returned path + includes a trailing slash. + This does not handle GRUB "(hd0,0)" paths properly yet since it only + looks at slashes. */ +char * +grub_get_dirname (const char *file_path) +{ + int i; + int last_slash; + + last_slash = -1; + for (i = grub_strlen (file_path) - 1; i >= 0; --i) + { + if (file_path[i] == '/') + { + last_slash = i; + break; + } + } + if (last_slash == -1) + return grub_strdup ("/"); + + return grub_new_substring (file_path, 0, last_slash + 1); +} + +static __inline int +isxdigit (char c) +{ + return ((c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F')); +} + +static int +parse_hex_color_component (const char *s, unsigned start, unsigned end) +{ + unsigned len; + char buf[3]; + + len = end - start; + /* Check the limits so we don't overrun the buffer. */ + if (len < 1 || len > 2) + return 0; + + if (len == 1) + { + buf[0] = s[start]; /* Get the first and only hex digit. */ + buf[1] = buf[0]; /* Duplicate the hex digit. */ + } + else if (len == 2) + { + buf[0] = s[start]; + buf[1] = s[start + 1]; + } + + buf[2] = '\0'; + + return grub_strtoul (buf, 0, 16); +} + +/* Parse a color string of the form "r, g, b", "#RGB", "#RGBA", + "#RRGGBB", or "#RRGGBBAA". */ +grub_gui_color_t +grub_gui_parse_color (const char *s) +{ + grub_gui_color_t c; + + /* Skip whitespace. */ + while (*s && grub_isspace (*s)) + s++; + + if (*s == '#') + { + /* HTML-style. Number if hex digits: + [6] #RRGGBB [3] #RGB + [8] #RRGGBBAA [4] #RGBA */ + + s++; /* Skip the '#'. */ + /* Count the hexits to determine the format. */ + int hexits = 0; + const char *end = s; + while (isxdigit (*end)) + { + end++; + hexits++; + } + + /* Parse the color components based on the format. */ + if (hexits == 3 || hexits == 4) + { + c.red = parse_hex_color_component (s, 0, 1); + c.green = parse_hex_color_component (s, 1, 2); + c.blue = parse_hex_color_component (s, 2, 3); + if (hexits == 4) + c.alpha = parse_hex_color_component (s, 3, 4); + else + c.alpha = 255; + } + else if (hexits == 6 || hexits == 8) + { + c.red = parse_hex_color_component (s, 0, 2); + c.green = parse_hex_color_component (s, 2, 4); + c.blue = parse_hex_color_component (s, 4, 6); + if (*s) + c.alpha = parse_hex_color_component (s, 6, 8); + else + c.alpha = 255; + } + else + goto fail; + } + else + { + /* Comma separated decimal values. */ + c.red = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + goto fail; + s++; + c.green = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + goto fail; + s++; + c.blue = grub_strtoul (s, 0, 0); + if ((s = grub_strchr (s, ',')) == 0) + c.alpha = 255; + else + { + s++; + c.alpha = grub_strtoul (s, 0, 0); + } + } + + return c; + +fail: + c.red = 0; + c.green = 0; + c.blue = 0; + c.alpha = 255; + return c; +} + +/* Parse a value in the form "(x, y)", storing the first element (x) into + *PX and the second element (y) into *PY. + Returns 1 if successful, 0 if failed to parse. */ +int +grub_gui_parse_2_tuple (const char *s, int *px, int *py) +{ + int x; + int y; + + while (*s && grub_isspace (*s)) + s++; + if (*s != '(') + return 0; + s++; /* Skip the opening parentheses. */ + if (! *s) + return 0; + x = grub_strtoul (s, 0, 10); + if ((s = grub_strchr (s, ',')) == 0) + return 0; + s++; /* Skip the element separator (the comma). */ + y = grub_strtoul (s, 0, 10); + + *px = x; + *py = y; + return 1; +} === added file 'gfxmenu/theme_loader.c' --- gfxmenu/theme_loader.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/theme_loader.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,587 @@ +/* theme_loader.c - Theme file loader for gfxmenu. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/* Construct a new box widget using PATTERN to find the pixmap files for it, + storing the new widget at *BOXPTR. PATTERN should be of the form: + "somewhere/style*.png". The '*' then gets substituted with the various + pixmap names that the widget uses. + Return zero on failure, nonzero on success. */ +static int +theme_set_box (grub_gfxmenu_box_t *boxptr, + const char *pattern, const char *theme_dir) +{ + char *abspattern; + char *prefix; + char *suffix; + char *star; + grub_gfxmenu_box_t box; + + abspattern = grub_resolve_relative_path (theme_dir, pattern); + if (! abspattern) + return 0; + + star = grub_strchr (abspattern, '*'); + if (! star) + { + grub_free (abspattern); + return 0; + } + + /* Prefix: Get the part before the '*'. */ + prefix = grub_malloc (star - abspattern + 1); + if (! prefix) + { + grub_free (abspattern); + return 0; + } + + grub_memcpy (prefix, abspattern, star - abspattern); + prefix[star - abspattern] = '\0'; + + /* Suffix: Everything after the '*' is the suffix. */ + suffix = star + 1; + + box = grub_gfxmenu_create_box (prefix, suffix); + grub_free (abspattern); /* Note: suffix, star point into abspattern. */ + grub_free (prefix); + if (! box) + return 0; + + if (*boxptr) + (*boxptr)->destroy (*boxptr); + *boxptr = box; + return 1; +} + +/* Set the specified property NAME on the view to the given string VALUE. + This function takes ownership of both NAME and VALUE, so the caller + should pass pointers to new heap-allocated strings. */ +static void +theme_set_string (grub_gfxmenu_view_t view, + const char *name, + const char *value, + const char *theme_dir) +{ + if (! grub_strcmp ("title-font", name)) + view->title_font = grub_font_get (value); + else if (! grub_strcmp ("item-font", name)) + view->item_font = grub_font_get (value); + else if (! grub_strcmp ("selected-item-font", name)) + view->selected_item_font = grub_font_get (value); + else if (! grub_strcmp ("status-font", name)) + view->status_font = grub_font_get (value); + else if (! grub_strcmp ("terminal-font", name)) + { + grub_free (view->terminal_font_name); + view->terminal_font_name = grub_strdup (value); + } + else if (! grub_strcmp ("title-color", name)) + view->title_color = grub_gui_parse_color (value); + else if (! grub_strcmp ("item-color", name)) + view->item_color = grub_gui_parse_color (value); + else if (! grub_strcmp ("selected-item-color", name)) + view->selected_item_color = grub_gui_parse_color (value); + else if (! grub_strcmp ("status-color", name)) + view->status_color = grub_gui_parse_color (value); + else if (! grub_strcmp ("status-bg-color", name)) + view->status_bg_color = grub_gui_parse_color (value); + else if (! grub_strcmp ("desktop-image", name)) + { + struct grub_video_bitmap *raw_bitmap; + struct grub_video_bitmap *scaled_bitmap; + char *path; + path = grub_resolve_relative_path (theme_dir, value); + if (! path) + return; + if (grub_video_bitmap_load (&raw_bitmap, path) != GRUB_ERR_NONE) + { + grub_free (path); + return; + } + grub_free(path); + grub_video_bitmap_create_scaled (&scaled_bitmap, + view->screen.width, + view->screen.height, + raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_video_bitmap_destroy (raw_bitmap); + if (!scaled_bitmap) + return; + + grub_video_bitmap_destroy (view->desktop_image); + view->desktop_image = scaled_bitmap; + } + else if (! grub_strcmp ("desktop-color", name)) + view->desktop_color = grub_gui_parse_color (value); + else if (! grub_strcmp ("menu-box", name)) + theme_set_box (&view->menu_box, value, theme_dir); + else if (! grub_strcmp ("selected-item-box", name)) + theme_set_box (&view->selected_item_box, value, theme_dir); + else if (! grub_strcmp ("terminal-box", name)) + theme_set_box (&view->terminal_box, value, theme_dir); + else if (! grub_strcmp ("title-text", name)) + { + grub_free (view->title_text); + view->title_text = grub_strdup (value); + } +} + +/* Set the specified property NAME on the view to the given numeric VALUE. + This function takes ownership NAME, so the caller should pass a pointer + to a new heap-allocated string. */ +static void +theme_set_number (grub_gfxmenu_view_t view, const char *name, int value) +{ + if (! grub_strcmp ("icon-width", name)) + view->icon_width = value; + else if (! grub_strcmp ("icon-height", name)) + view->icon_height = value; + else if (! grub_strcmp ("item-height", name)) + view->item_height = value; + else if (! grub_strcmp ("item-padding", name)) + view->item_padding = value; + else if (! grub_strcmp ("item-icon-space", name)) + view->item_icon_space = value; + else if (! grub_strcmp ("item-spacing", name)) + view->item_spacing = value; +} + +struct parsebuf +{ + char *buf; + int pos; + int len; + char *theme_dir; + grub_gfxmenu_view_t view; +}; + +static void +advance_to_next_line (struct parsebuf *p) +{ + /* Eat characters up to the newline. */ + while (p->pos < p->len && p->buf[p->pos] != '\n') + p->pos++; + p->pos++; /* Eat the newline. */ +} + +static void +skip_whitespace (struct parsebuf *p) +{ + while (p->pos < p->len + && (p->buf[p->pos] == ' ' + || p->buf[p->pos] == '\t' + || p->buf[p->pos] == '\r' + || p->buf[p->pos] == '\n' + || p->buf[p->pos] == '\f')) + p->pos++; +} + +static int +read_char (struct parsebuf *p) +{ + if (p->pos < p->len) + return p->buf[p->pos++]; + else + return -1; +} + +static char * +read_identifier (struct parsebuf *p) +{ + int start; + int end; + + skip_whitespace (p); + start = p->pos; + while (p->pos < p->len + && (grub_isalpha(p->buf[p->pos]) + || grub_isdigit(p->buf[p->pos]) + || p->buf[p->pos] == '_' + || p->buf[p->pos] == '-')) + p->pos++; + end = p->pos; + if (end - start < 1) + return 0; + + return grub_new_substring (p->buf, start, end); +} + +static char * +read_expression (struct parsebuf *p) +{ + int start; + int end; + + skip_whitespace (p); + if (p->pos < p->len && p->buf[p->pos] == '"') + { + /* Read as a quoted string. */ + /* The quotation marks are not included in the expression value. */ + p->pos++; + start = p->pos; + while (p->pos < p->len + && p->buf[p->pos] != '"') + p->pos++; + end = p->pos; + if (p->pos < p->len) + p->pos++; /* Skip the terminating quotation mark. */ + } + else if (p->pos < p->len && p->buf[p->pos] == '(') + { + /* Read as a parenthesized string -- for tuples/coordinates. */ + /* The parentheses are included in the expression value. */ + start = p->pos; + while (p->pos < p->len) + { + char c = p->buf[p->pos]; + p->pos++; + if (c == ')') + break; + } + end = p->pos; + } + else if (p->pos < p->len) + { + /* Read as a single word -- for numeric values. */ + start = p->pos; + while (p->pos < p->len + && ! grub_isspace (p->buf[p->pos])) + p->pos++; + end = p->pos; + } + else + return 0; + + return grub_new_substring (p->buf, start, end); +} + +/* Read a GUI object specification from the theme file. + Any components created will be added to the GUI container PARENT. + Returns 0 on success, nonzero on failure. */ +static int +read_object (struct parsebuf *p, grub_gui_container_t parent) +{ + char *name; + name = read_identifier (p); + if (! name) + goto fail; + + grub_gui_component_t component = 0; + if (grub_strcmp (name, "label") == 0) + { + component = grub_gui_label_new (); + } + else if (grub_strcmp (name, "image") == 0) + { + component = grub_gui_image_new (); + } + else if (grub_strcmp (name, "vbox") == 0) + { + component = (grub_gui_component_t) grub_gui_vbox_new (); + } + else if (grub_strcmp (name, "hbox") == 0) + { + component = (grub_gui_component_t) grub_gui_hbox_new (); + } + else if (grub_strcmp (name, "canvas") == 0) + { + component = (grub_gui_component_t) grub_gui_canvas_new (); + } + else if (grub_strcmp (name, "progress_bar") == 0) + { + component = (grub_gui_component_t) grub_gui_progress_bar_new (); + } + else + { + /* Unknown type; ignore. */ + goto fail; /* We should actually parse the structure. */ + } + + if (component) + { + grub_video_rect_t r = { .x=0, .y=0, .width=-1, .height=-1 }; + component->ops->set_bounds (component, &r); + parent->ops->add (parent, component); + } + + skip_whitespace (p); + if (read_char (p) != '{') + goto fail; + + while (p->pos < p->len) + { + skip_whitespace (p); + + /* Check whether the end has been encountered. */ + if (p->pos < p->len + && p->buf[p->pos] == '}') + { + p->pos++; /* Skip the closing brace. */ + break; + } + + if (p->pos < p->len + && p->buf[p->pos] == '#') + { + /* Skip comments. */ + advance_to_next_line (p); + continue; + } + + if (p->pos < p->len + && p->buf[p->pos] == '+') + { + p->pos++; /* Skip the '+'. */ + /* Check whether this component is a container. */ + if (component->ops->is_instance (component, "container")) + { + /* Read the sub-object recursively and add it as a child. */ + if (read_object (p, (grub_gui_container_t) component) != 0) + goto fail; + /* After reading the sub-object, resume parsing, expecting + another property assignment or sub-object definition. */ + continue; + } + else + { + /* error ("Attempted to add object to non-container.") */ + goto fail; + } + } + + char *property; + property = read_identifier (p); + if (! property) + goto fail; + + skip_whitespace (p); + if (read_char (p) != '=') + { + grub_free (property); + goto fail; + } + skip_whitespace (p); + + char *value; + value = read_expression (p); + if (! value) + { + grub_free (property); + goto fail; + } + + /* Handle the property value. */ + if (grub_strcmp (property, "position") == 0) + { + /* Special case for position value. */ + int x; + int y; + + if (grub_gui_parse_2_tuple (value, &x, &y)) + { + grub_video_rect_t r; + component->ops->get_bounds (component, &r); + r.x = x; + r.y = y; + component->ops->set_bounds (component, &r); + } + } + else if (grub_strcmp (property, "size") == 0) + { + /* Special case for size value. */ + int w; + int h; + + if (grub_gui_parse_2_tuple (value, &w, &h)) + { + grub_video_rect_t r; + component->ops->get_bounds (component, &r); + r.width = w; + r.height = h; + component->ops->set_bounds (component, &r); + } + } + else + { + /* General property handling. */ + component->ops->set_property (component, property, value); + } + + grub_free (value); + grub_free (property); + } + + /* Set the object's size to its preferred size unless the user has + explicitly specified the size. */ + grub_video_rect_t r; + component->ops->get_bounds (component, &r); + if (r.width == -1 || r.height == -1) + { + component->ops->get_preferred_size (component, &r.width, &r.height); + component->ops->set_bounds (component, &r); + } + + return 0; + +fail: + grub_free (name); + + /* Once the opening brace has been found, we always jump past the closing + brace if an error is encountered. */ + while (p->pos < p->len + && p->buf[p->pos] != '}') + p->pos++; + return 1; +} + +static void +read_property (struct parsebuf *p) +{ + char *name; + + name = read_identifier (p); /* Read the property name. */ + if (! name) + goto next_line; + + skip_whitespace (p); /* Skip whitespace before separator. */ + + if (read_char (p) != ':') /* Read separator. */ + goto next_line; + + skip_whitespace (p); /* Skip whitespace after separator. */ + + /* Get the value based on its type. */ + if (p->pos < p->len && p->buf[p->pos] == '"') + { + /* String value. (e.g., '"My string"') */ + char *value = read_expression (p); + if (! value) + goto next_line; + theme_set_string (p->view, name, value, p->theme_dir); + grub_free (value); + } + else if (p->pos < p->len && grub_isdigit(p->buf[p->pos])) + { + /* Numeric value. (e.g., '123') */ + int value; + char *value_str = read_expression (p); + if (! value_str) + goto next_line; + + value = grub_strtoul (value_str, 0, 0); + grub_free (value_str); + theme_set_number (p->view, name, value); + } + +next_line: + grub_free (name); + advance_to_next_line (p); +} + +/* Set properties on the view based on settings from the specified + theme file. Returns nonzero on success, zero on failure. */ +int +grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, const char *theme_path) +{ + grub_file_t file; + struct parsebuf p; + + p.view = view; + p.theme_dir = grub_get_dirname (theme_path); + + file = grub_file_open (theme_path); + if (!file) + { + grub_free (p.theme_dir); + return 0; + } + + p.len = grub_file_size (file); + p.buf = grub_malloc (p.len); + if (! p.buf) + { + grub_file_close (file); + grub_free (p.theme_dir); + return 0; + } + if (grub_file_read (file, p.buf, p.len) != p.len) + { + grub_free (p.buf); + grub_file_close (file); + grub_free (p.theme_dir); + return 0; + } + + if (view->canvas) + view->canvas->ops->component.destroy (view->canvas); + view->canvas = grub_gui_canvas_new (); + + p.pos = 0; + while (p.pos < p.len) + { + /* Skip comments (lines beginning with #). */ + if (p.pos < p.len && p.buf[p.pos] == '#') + { + advance_to_next_line (&p); + continue; + } + + /* Get name. */ + /* Find the first non-whitespace character. */ + skip_whitespace (&p); + + /* Handle the content. */ + if (p.buf[p.pos] == '+') + { + p.pos++; /* Skip the '+'. */ + read_object (&p, view->canvas); + } + else + { + read_property (&p); + } + } + + /* Set the new theme path. */ + grub_free (view->theme_path); + view->theme_path = grub_strdup (theme_path); + + /* There may be new icons for this theme. */ + grub_gfxmenu_view_free_icon_cache (view); + + grub_free (p.buf); + grub_file_close (file); + grub_free (p.theme_dir); + return 1; +} === added file 'gfxmenu/view.c' --- gfxmenu/view.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/view.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,725 @@ +/* view.c - Graphical menu interface MVC view. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* The component ID identifying GUI components to be updated as the timeout + status changes. */ +#define TIMEOUT_COMPONENT_ID "__timeout__" + +static void init_terminal (grub_gfxmenu_view_t view); +static void destroy_terminal (void); +static grub_err_t set_graphics_mode (void); +static grub_err_t set_text_mode (void); + +/* Create a new view object, loading the theme specified by THEME_PATH and + associating MODEL with the view. */ +grub_gfxmenu_view_t +grub_gfxmenu_view_new (const char *theme_path, grub_gfxmenu_model_t model) +{ + grub_gfxmenu_view_t view; + + view = grub_malloc (sizeof (*view)); + if (! view) + return 0; + + set_graphics_mode (); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + grub_video_get_viewport ((unsigned *) &view->screen.x, + (unsigned *) &view->screen.y, + (unsigned *) &view->screen.width, + (unsigned *) &view->screen.height); + + /* Clear the screen; there may be garbage left over in video memory, and + loading the menu style (particularly the background) can take a while. */ + grub_video_fill_rect (grub_video_map_rgb (0, 0, 0), + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + grub_video_swap_buffers (); + + grub_font_t default_font; + grub_gui_color_t default_fg_color; + grub_gui_color_t default_bg_color; + + default_font = grub_font_get ("Helvetica 12"); + default_fg_color = grub_gui_color_rgb (0, 0, 0); + default_bg_color = grub_gui_color_rgb (255, 255, 255); + + view->icon_width = 32; + view->icon_height = 32; + view->item_height = 42; + view->item_padding = 14; + view->item_icon_space = 4; + view->item_spacing = 16; + view->title_font = default_font; + view->item_font = default_font; + view->selected_item_font = default_font; + view->status_font = default_font; + view->terminal_font_name = grub_strdup ("Fixed 10"); + view->title_color = default_fg_color; + view->item_color = default_fg_color; + view->selected_item_color = default_fg_color; + view->status_color = default_bg_color; + view->status_bg_color = default_fg_color; + view->desktop_image = 0; + view->desktop_color = default_bg_color; + view->menu_box = 0; + view->selected_item_box = 0; + view->terminal_box = 0; + view->title_text = grub_strdup ("GRUB Boot Menu"); + view->progress_message_text = 0; + view->theme_path = 0; + view->icon_cache.class_name = 0; + view->icon_cache.bitmap = 0; + view->icon_cache.next = 0; + + view->model = model; + + view->canvas = 0; + + if (! grub_gfxmenu_view_load_theme (view, theme_path)) + { + grub_gfxmenu_view_destroy (view); + return 0; + } + + init_terminal (view); + + return view; +} + +void +grub_gfxmenu_view_free_icon_cache (grub_gfxmenu_view_t view) +{ + grub_gfxmenu_view_icon_t cur; + grub_gfxmenu_view_icon_t next; + for (cur = view->icon_cache.next; cur; cur = next) + { + next = cur->next; + grub_free (cur); + } + view->icon_cache.next = 0; +} + +/* Destroy the view object. All used memory is freed. */ +void +grub_gfxmenu_view_destroy (grub_gfxmenu_view_t view) +{ + grub_video_bitmap_destroy (view->desktop_image); + if (view->menu_box) + view->menu_box->destroy (view->menu_box); + if (view->selected_item_box) + view->selected_item_box->destroy (view->selected_item_box); + if (view->terminal_box) + view->terminal_box->destroy (view->terminal_box); + grub_free (view->terminal_font_name); + grub_free (view->title_text); + grub_free (view->progress_message_text); + grub_free (view->theme_path); + grub_gfxmenu_view_free_icon_cache (view); + if (view->canvas) + view->canvas->ops->component.destroy (view->canvas); + grub_free (view); + + set_text_mode (); + destroy_terminal (); +} + +/* Sets MESSAGE as the progress message for the view. + MESSAGE can be 0, in which case no message is displayed. */ +static void +set_progress_message (grub_gfxmenu_view_t view, const char *message) +{ + grub_free (view->progress_message_text); + if (message) + view->progress_message_text = grub_strdup (message); + else + view->progress_message_text = 0; +} + +static void +draw_background (grub_gfxmenu_view_t view) +{ + if (view->desktop_image) + { + struct grub_video_bitmap *img = view->desktop_image; + grub_video_blit_bitmap (img, GRUB_VIDEO_BLIT_REPLACE, + view->screen.x, view->screen.y, 0, 0, + grub_video_bitmap_get_width (img), + grub_video_bitmap_get_height (img)); + } + else + { + grub_video_fill_rect (grub_gui_map_color (view->desktop_color), + view->screen.x, view->screen.y, + view->screen.width, view->screen.height); + } +} + +static const char icon_extension[] = ".png"; + +static struct grub_video_bitmap * +try_loading_icon (grub_gfxmenu_view_t view, + const char *dir, const char *class_name) +{ + char *path = grub_malloc (grub_strlen (dir) + + grub_strlen (class_name) + + grub_strlen (icon_extension) + + 1); + if (! path) + return 0; + + grub_strcpy (path, dir); + grub_strcat (path, class_name); + grub_strcat (path, icon_extension); + + struct grub_video_bitmap *raw_bitmap; + grub_video_bitmap_load (&raw_bitmap, path); + grub_free (path); + grub_errno = GRUB_ERR_NONE; /* Critical to clear the error!! */ + if (! raw_bitmap) + return 0; + + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + view->icon_width, view->icon_height, + raw_bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + grub_video_bitmap_destroy (raw_bitmap); + if (! scaled_bitmap) + return 0; + + return scaled_bitmap; +} + +static struct grub_video_bitmap * +get_icon_by_class (grub_gfxmenu_view_t view, const char *class_name) +{ + + /* First check the view's icon cache. */ + grub_gfxmenu_view_icon_t vicon; + for (vicon = view->icon_cache.next; vicon; vicon = vicon->next) + { + if (grub_strcmp (vicon->class_name, class_name) == 0) + return vicon->bitmap; + } + + /* Otherwise, we search for an icon to load. */ + if (! view->theme_path) + return 0; + + char *theme_dir = grub_get_dirname (view->theme_path); + char *icons_dir; + struct grub_video_bitmap *icon; + icon = 0; + /* First try the theme's own icons, from "grub/themes/NAME/icons/" */ + icons_dir = grub_resolve_relative_path (theme_dir, "icons/"); + if (icons_dir) + { + icon = try_loading_icon (view, icons_dir, class_name); + grub_free (icons_dir); + } + if (! icon) + { + /* If the theme doesn't have an appropriate icon, check in + "grub/themes/icons". */ + icons_dir = grub_resolve_relative_path (theme_dir, "../icons/"); + if (icons_dir) + { + icon = try_loading_icon (view, icons_dir, class_name); + grub_free (icons_dir); + } + } + grub_free (theme_dir); + + if (! icon) + return 0; + + vicon = grub_malloc (sizeof (*vicon)); + if (! vicon) + { + grub_video_bitmap_destroy (icon); + return 0; + } + vicon->class_name = class_name; + vicon->bitmap = icon; + vicon->next = view->icon_cache.next; + view->icon_cache.next = vicon; /* Link it into the cache. */ + return vicon->bitmap; +} + +static struct grub_video_bitmap * +get_item_icon (grub_gfxmenu_view_t view, int item_index) +{ + grub_menu_entry_t entry; + entry = grub_gfxmenu_model_get_entry (view->model, item_index); + if (! entry) + return 0; + + struct grub_menu_entry_class *c; + struct grub_video_bitmap *icon; + icon = 0; + for (c = entry->classes->next; c && ! icon; c = c->next) + icon = get_icon_by_class (view, c->name); + return icon; +} + +static void +draw_menu (grub_gfxmenu_view_t view) +{ + int boxpad = view->item_padding; + int icon_text_space = view->item_icon_space; + int item_vspace = view->item_spacing; + + int ascent = grub_font_get_ascent (view->item_font); + int descent = grub_font_get_descent (view->item_font); + int item_height = view->item_height; + + int num_items = grub_gfxmenu_model_get_num_entries (view->model); + grub_video_rect_t r; + r.width = view->screen.width * 4 / 5; + /* Set the menu box height to fit the items. */ + r.height = (item_height * num_items + + item_vspace * (num_items - 1) + + 2 * boxpad); + r.x = (view->screen.width - r.width) / 2; + r.y = (view->screen.height - r.height) / 2; + view->menu_box->set_content_size (view->menu_box, r.width, r.height); + + int menu_box_left_pad = view->menu_box->get_left_pad (view->menu_box); + int menu_box_top_pad = view->menu_box->get_top_pad (view->menu_box); + view->menu_box->draw (view->menu_box, + r.x - menu_box_left_pad, r.y - menu_box_top_pad); + + int item_top = r.y + boxpad; + int item_left = r.x + boxpad; + int i; + + for (i = 0; i < num_items; i++) + { + int is_selected = + (i == grub_gfxmenu_model_get_selected_index (view->model)); + + if (is_selected) + { + view->selected_item_box->set_content_size (view->selected_item_box, + r.width - 2 * boxpad, + item_height); + int leftpad = view->selected_item_box->get_left_pad (view->selected_item_box); + int toppad = view->selected_item_box->get_top_pad (view->selected_item_box); + view->selected_item_box->draw (view->selected_item_box, + item_left - leftpad, + item_top - toppad); + } + + struct grub_video_bitmap *icon; + if ((icon = get_item_icon (view, i)) != 0) + grub_video_blit_bitmap (icon, GRUB_VIDEO_BLIT_BLEND, + item_left, + item_top + (item_height - view->icon_height) / 2, + 0, 0, view->icon_width, view->icon_height); + + const char *item_title = + grub_gfxmenu_model_get_entry_title (view->model, i); + grub_video_draw_string (item_title, + (is_selected + ? view->selected_item_font + : view->item_font), + grub_gui_map_color (is_selected + ? view->selected_item_color + : view->item_color), + item_left + view->icon_width + icon_text_space, + (item_top + (item_height - (ascent + descent)) + / 2 + ascent)); + + item_top += item_height + item_vspace; + } +} + +static void +draw_title (grub_gfxmenu_view_t view) +{ + if (! view->title_text) + return; + + /* Center the title. */ + int title_width = grub_font_get_string_width (view->title_font, + view->title_text); + int x = (view->screen.width - title_width) / 2; + int y = 40 + grub_font_get_ascent (view->title_font); + grub_video_draw_string (view->title_text, + view->title_font, + grub_gui_map_color (view->title_color), + x, y); +} + +static void +draw_status (grub_gfxmenu_view_t view) +{ + int descent = grub_font_get_descent (view->status_font); + int ascent = grub_font_get_ascent (view->status_font); + int vpad = 5; + int textheight = descent + ascent + 1; + int h = 2 * vpad + 2 * textheight; + + grub_video_fill_rect (grub_gui_map_color (view->status_bg_color), + 0, view->screen.height - h, + view->screen.width, view->screen.height - 1); + + int texty = view->screen.height - h + vpad + ascent; + grub_video_draw_string ("Select an item with the arrow keys and " + "press Enter to boot.", + view->status_font, + grub_gui_map_color (view->status_color), + 30, texty); + texty += textheight; + grub_video_draw_string ("Press: 'c' for command line; 't' to switch to " + "non-graphical menu.", + view->status_font, + grub_gui_map_color (view->status_color), + 30, texty); +} + +struct progress_value_data +{ + const char *visible; + const char *start; + const char *end; + const char *value; + const char *text; +}; + +static void +update_timeout_visit (grub_gui_component_t component, + void *userdata) +{ + struct progress_value_data *pv; + pv = (struct progress_value_data *) userdata; + component->ops->set_property (component, "visible", pv->visible); + component->ops->set_property (component, "start", pv->start); + component->ops->set_property (component, "end", pv->end); + component->ops->set_property (component, "value", pv->value); + component->ops->set_property (component, "text", pv->text); +} + +static void +update_timeout (grub_gfxmenu_view_t view) +{ + char startbuf[20]; + char valuebuf[20]; + char msgbuf[120]; + + int timeout = grub_gfxmenu_model_get_timeout_ms (view->model); + int remaining = grub_gfxmenu_model_get_timeout_remaining_ms (view->model); + struct progress_value_data pv; + + pv.visible = timeout > 0 ? "true" : "false"; + grub_sprintf (startbuf, "%d", -timeout); + pv.start = startbuf; + pv.end = "0"; + grub_sprintf (valuebuf, "%d", remaining > 0 ? -remaining : 0); + pv.value = valuebuf; + + int seconds_remaining_rounded_up = (remaining + 999) / 1000; + grub_sprintf (msgbuf, + "The highlighted entry will be booted automatically in %d s.", + seconds_remaining_rounded_up); + pv.text = msgbuf; + + grub_gui_find_by_id ((grub_gui_component_t) view->canvas, + TIMEOUT_COMPONENT_ID, update_timeout_visit, &pv); +} + +static void +draw_message (grub_gfxmenu_view_t view) +{ + char *text = view->progress_message_text; + if (! text) + return; + + grub_font_t font = view->status_font; + grub_video_color_t color = grub_gui_map_color (view->status_color); + + /* Set the timeout bar's frame. */ + grub_video_rect_t f; + f.width = view->screen.width * 4 / 5; + f.height = 50; + f.x = view->screen.x + (view->screen.width - f.width) / 2; + f.y = view->screen.y + view->screen.height - 90 - 20 - f.height; + + /* Border. */ + grub_video_fill_rect (color, + f.x-1, f.y-1, f.width+2, f.height+2); + /* Fill. */ + grub_video_fill_rect (grub_gui_map_color (view->status_bg_color), + f.x, f.y, f.width, f.height); + + /* Center the text. */ + int text_width = grub_font_get_string_width (font, text); + int x = f.x + (f.width - text_width) / 2; + int y = (f.y + (f.height - grub_font_get_descent (font)) / 2 + + grub_font_get_ascent (font) / 2); + grub_video_draw_string (text, font, color, x, y); +} + + +void +grub_gfxmenu_view_draw (grub_gfxmenu_view_t view) +{ + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + update_timeout (view); + + draw_background (view); + if (view->canvas) + view->canvas->ops->component.paint (view->canvas); + draw_menu (view); + draw_title (view); + draw_status (view); + draw_message (view); +} + +static grub_err_t +set_graphics_mode (void) +{ + const char *doublebuf_str = grub_env_get ("doublebuffering"); + int doublebuf_flags = + (doublebuf_str && doublebuf_str[0] == 'n') + ? 0 + : GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + + const char *modestr = grub_env_get ("gfxmode"); + if (grub_video_setup_preferred_mode (modestr, doublebuf_flags, 640, 480) + != GRUB_ERR_NONE) + return grub_errno; + + return GRUB_ERR_NONE; +} + +static grub_err_t +set_text_mode (void) +{ + return grub_video_restore (); +} + +static int term_target_width; +static int term_target_height; +static struct grub_video_render_target *term_target; +static int term_initialized; +static grub_term_t term_original; +static grub_gfxmenu_view_t term_view; + +static void +repaint_terminal (int x __attribute ((unused)), + int y __attribute ((unused)), + int width __attribute ((unused)), + int height __attribute ((unused))) +{ + if (! term_view) + return; + + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + grub_gfxmenu_view_draw (term_view); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + int termx = term_view->screen.x + + term_view->screen.width * (10 - 7) / 10 / 2; + int termy = term_view->screen.y + + term_view->screen.height * (10 - 7) / 10 / 2; + + grub_gfxmenu_box_t term_box = term_view->terminal_box; + if (term_box) + { + term_box->set_content_size (term_box, + term_target_width, term_target_height); + + term_box->draw (term_box, + termx - term_box->get_left_pad (term_box), + termy - term_box->get_top_pad (term_box)); + } + + grub_video_blit_render_target (term_target, GRUB_VIDEO_BLIT_REPLACE, + termx, termy, + 0, 0, term_target_width, term_target_height); + grub_video_swap_buffers (); +} + +static void +init_terminal (grub_gfxmenu_view_t view) +{ + term_original = grub_term_get_current (); + + term_target_width = view->screen.width * 7 / 10; + term_target_height = view->screen.height * 7 / 10; + + grub_video_create_render_target (&term_target, + term_target_width, + term_target_height, + GRUB_VIDEO_MODE_TYPE_RGB + | GRUB_VIDEO_MODE_TYPE_ALPHA); + if (grub_errno != GRUB_ERR_NONE) + return; + + /* Note: currently there is no API for changing the gfxterm font + on the fly, so whatever font the initially loaded theme specifies + will be permanent. */ + grub_gfxterm_init_window (term_target, 0, 0, + term_target_width, term_target_height, + view->terminal_font_name, 3); + if (grub_errno != GRUB_ERR_NONE) + return; + term_initialized = 1; + + /* XXX: store static pointer to the 'view' object so the repaint callback can access it. */ + term_view = view; + grub_gfxterm_set_repaint_callback (repaint_terminal); + grub_term_set_current (grub_gfxterm_get_term ()); +} + +static void destroy_terminal (void) +{ + term_view = 0; + if (term_initialized) + grub_gfxterm_destroy_window (); + grub_gfxterm_set_repaint_callback (0); + if (term_target) + grub_video_delete_render_target (term_target); + if (term_original) + grub_term_set_current (term_original); +} + + +static void +notify_booting (void *userdata, grub_menu_entry_t entry) +{ + grub_gfxmenu_view_t view = (grub_gfxmenu_view_t) userdata; + + char *s = grub_malloc (100 + grub_strlen (entry->title)); + if (!s) + return; + + grub_sprintf (s, "Booting '%s'", entry->title); + set_progress_message (view, s); + grub_free (s); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); +} + +static void +notify_fallback (void *userdata, grub_menu_entry_t entry) +{ + grub_gfxmenu_view_t view = (grub_gfxmenu_view_t) userdata; + + char *s = grub_malloc (100 + grub_strlen (entry->title)); + if (!s) + return; + + grub_sprintf (s, "Falling back to '%s'", entry->title); + set_progress_message (view, s); + grub_free (s); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); +} + +static void +notify_execution_failure (void *userdata __attribute__ ((unused))) +{ +} + + +static struct grub_menu_execute_callback execute_callback = +{ + .notify_booting = notify_booting, + .notify_fallback = notify_fallback, + .notify_failure = notify_execution_failure +}; + +int +grub_gfxmenu_view_execute_with_fallback (grub_gfxmenu_view_t view, + grub_menu_entry_t entry) +{ + grub_menu_execute_with_fallback (grub_gfxmenu_model_get_menu (view->model), + entry, &execute_callback, (void *) view); + + if (set_graphics_mode () != GRUB_ERR_NONE) + return 0; /* Failure. */ + + /* If we returned, there was a failure. */ + set_progress_message (view, + "Unable to automatically boot. " + "Press SPACE to continue."); + grub_gfxmenu_view_draw (view); + grub_video_swap_buffers (); + while (GRUB_TERM_ASCII_CHAR(grub_getkey ()) != ' ') + { + /* Wait for SPACE to be pressed. */ + } + + set_progress_message (view, 0); /* Clear the message. */ + + return 1; /* Ok. */ +} + +int +grub_gfxmenu_view_execute_entry (grub_gfxmenu_view_t view, + grub_menu_entry_t entry) +{ + /* Currently we switch back to text mode by restoring + the original terminal before executing the menu entry. + It is hard to make it work when executing a menu entry + that switches video modes -- it using gfxterm in a + window, the repaint callback seems to crash GRUB. */ + /* TODO: Determine if this works when 'gfxterm' was set as + the current terminal before invoking the gfxmenu. */ + destroy_terminal (); + + grub_menu_execute_entry (entry); + if (grub_errno != GRUB_ERR_NONE) + grub_wait_after_message (); + + if (set_graphics_mode () != GRUB_ERR_NONE) + return 0; /* Failure. */ + + init_terminal (view); + return 1; /* Ok. */ +} + +void +grub_gfxmenu_view_run_terminal (grub_gfxmenu_view_t view __attribute__((unused))) +{ + grub_cmdline_run (1); +} + === added file 'gfxmenu/widget-box.c' --- gfxmenu/widget-box.c 1970-01-01 00:00:00 +0000 +++ gfxmenu/widget-box.c 2008-08-12 02:55:06 +0000 @@ -0,0 +1,243 @@ +/* widget_box.c - Pixmap-stylized box widget. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +enum box_pixmaps +{ + BOX_PIXMAP_NW, BOX_PIXMAP_NE, BOX_PIXMAP_SE, BOX_PIXMAP_SW, + BOX_PIXMAP_N, BOX_PIXMAP_E, BOX_PIXMAP_S, BOX_PIXMAP_W, + BOX_PIXMAP_CENTER +}; + +static const char *box_pixmap_names[] = { + /* Corners: */ + "nw", "ne", "se", "sw", + /* Sides: */ + "n", "e", "s", "w", + /* Center: */ + "c" +}; + +#define BOX_NUM_PIXMAPS (sizeof(box_pixmap_names)/sizeof(*box_pixmap_names)) + +static void +draw (grub_gfxmenu_box_t self, int x, int y) +{ + int height_n; + int height_s; + int height_e; + int height_w; + int width_n; + int width_s; + int width_e; + int width_w; + unsigned i; + + /* Don't try to draw if any pixmaps are null, except the center. */ + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + if (i != BOX_PIXMAP_CENTER && self->scaled_pixmaps[i] == 0) + return; + } + + height_n = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_N]); + height_s = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_S]); + height_e = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_E]); + height_w = grub_video_bitmap_get_height (self->scaled_pixmaps[BOX_PIXMAP_W]); + width_n = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_N]); + width_s = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_S]); + width_e = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_E]); + width_w = grub_video_bitmap_get_width (self->scaled_pixmaps[BOX_PIXMAP_W]); + + /* Draw sides. */ + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_N], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y, 0, 0, width_n, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_S], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y + height_n + height_w, + 0, 0, width_s, height_s); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_E], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y + height_n, + 0, 0, width_e, height_e); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_W], + GRUB_VIDEO_BLIT_BLEND, + x, y + height_n, 0, 0, width_w, height_w); + + /* Draw corners. */ + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_NW], + GRUB_VIDEO_BLIT_BLEND, + x, y, 0, 0, width_w, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_NE], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y, + 0, 0, width_e, height_n); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_SE], + GRUB_VIDEO_BLIT_BLEND, + x + width_w + width_n, y + height_n + height_e, + 0, 0, width_e, height_s); + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_SW], + GRUB_VIDEO_BLIT_BLEND, + x, y + height_n + height_w, + 0, 0, width_w, height_s); + + /* Draw center. */ + if (self->scaled_pixmaps[BOX_PIXMAP_CENTER]) + grub_video_blit_bitmap (self->scaled_pixmaps[BOX_PIXMAP_CENTER], + GRUB_VIDEO_BLIT_BLEND, + x + width_w, y + height_n, + 0, 0, self->width, self->height); +} + +static void +scale_pixmap (grub_gfxmenu_box_t self, int i, int w, int h) +{ + struct grub_video_bitmap **scaled = &self->scaled_pixmaps[i]; + struct grub_video_bitmap *raw = self->raw_pixmaps[i]; + + if (raw == 0) + return; + + if (w == -1) + w = grub_video_bitmap_get_width (raw); + if (h == -1) + h = grub_video_bitmap_get_height (raw); + + if (*scaled == 0 + || ((int) grub_video_bitmap_get_width (*scaled) != w) + || ((int) grub_video_bitmap_get_height (*scaled) != h)) + { + if (*scaled) + { + grub_video_bitmap_destroy (*scaled); + *scaled = 0; + } + grub_video_bitmap_create_scaled (scaled, w, h, raw, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + } +} + +static void +set_content_size (grub_gfxmenu_box_t self, + int width, int height) +{ + self->width = width; + self->height = height; + /* Resize sides to match the width and height. */ + /* It is assumed that the corners width/height match the adjacent sides. */ + + /* Resize N and S sides to match width. */ + scale_pixmap(self, BOX_PIXMAP_N, width, -1); + scale_pixmap(self, BOX_PIXMAP_S, width, -1); + + /* Resize E and W sides to match height. */ + scale_pixmap(self, BOX_PIXMAP_E, -1, height); + scale_pixmap(self, BOX_PIXMAP_W, -1, height); + + /* Don't scale the corners--they are assumed to match the sides. */ + scale_pixmap(self, BOX_PIXMAP_NW, -1, -1); + scale_pixmap(self, BOX_PIXMAP_SW, -1, -1); + scale_pixmap(self, BOX_PIXMAP_NE, -1, -1); + scale_pixmap(self, BOX_PIXMAP_SE, -1, -1); + + /* Scale the center area. */ + scale_pixmap(self, BOX_PIXMAP_CENTER, width, height); +} + +static int +get_left_pad (grub_gfxmenu_box_t self) +{ + return grub_video_bitmap_get_width (self->raw_pixmaps[BOX_PIXMAP_W]); +} + +static int +get_top_pad (grub_gfxmenu_box_t self) +{ + return grub_video_bitmap_get_height (self->raw_pixmaps[BOX_PIXMAP_N]); +} + +static void +destroy (grub_gfxmenu_box_t self) +{ + unsigned i; + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + if (self->raw_pixmaps[i]) + grub_video_bitmap_destroy(self->raw_pixmaps[i]); + self->raw_pixmaps[i] = 0; + + if (self->scaled_pixmaps[i]) + grub_video_bitmap_destroy(self->scaled_pixmaps[i]); + self->scaled_pixmaps[i] = 0; + } + grub_free (self->raw_pixmaps); + self->raw_pixmaps = 0; + grub_free (self->scaled_pixmaps); + self->scaled_pixmaps = 0; + + grub_free (self); /* Free self: must be the last step! */ +} + + +grub_gfxmenu_box_t +grub_gfxmenu_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix) +{ + char path[200]; + unsigned i; + grub_gfxmenu_box_t box; + + box = (grub_gfxmenu_box_t) grub_malloc (sizeof (*box)); + if (!box) + return 0; + box->width = 0; + box->height = 0; + box->raw_pixmaps = + (struct grub_video_bitmap **) + grub_malloc (BOX_NUM_PIXMAPS * sizeof (struct grub_video_bitmap *)); + box->scaled_pixmaps = + (struct grub_video_bitmap **) + grub_malloc (BOX_NUM_PIXMAPS * sizeof (struct grub_video_bitmap *)); + + for (i = 0; i < BOX_NUM_PIXMAPS; i++) + { + /* TODO XXX dynamically allocate PATH, making sure it's large enough */ + grub_sprintf (path, "%s%s%s", + pixmaps_prefix, box_pixmap_names[i], pixmaps_suffix); + grub_video_bitmap_load (&box->raw_pixmaps[i], path); + box->scaled_pixmaps[i] = 0; + } + + box->draw = draw; + box->set_content_size = set_content_size; + box->get_left_pad = get_left_pad; + box->get_top_pad = get_top_pad; + box->destroy = destroy; + + return box; +} === added file 'include/grub/bitmap_scale.h' --- include/grub/bitmap_scale.h 1970-01-01 00:00:00 +0000 +++ include/grub/bitmap_scale.h 2008-07-03 14:27:43 +0000 @@ -0,0 +1,54 @@ +/* bitmap_scale.h - Bitmap scaling functions. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_BITMAP_SCALE_HEADER +#define GRUB_BITMAP_SCALE_HEADER 1 + +#include +#include +#include + +enum grub_video_bitmap_scale_method +{ + /* Choose the fastest interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST, + /* Choose the highest quality interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST, + /* Nearest neighbor interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST, + /* Bilinear interpolation algorithm. */ + GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR +}; + +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src); + +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum + grub_video_bitmap_scale_method scale_method); + +#endif /* ! GRUB_BITMAP_SCALE_HEADER */ === modified file 'include/grub/font.h' --- include/grub/font.h 2007-07-21 22:32:33 +0000 +++ include/grub/font.h 2008-07-03 14:17:31 +0000 @@ -1,6 +1,6 @@ /* * GRUB -- GRand Unified Bootloader - * Copyright (C) 2003,2007 Free Software Foundation, Inc. + * Copyright (C) 2003,2007,2008 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,33 +21,85 @@ #include -#define GRUB_FONT_MAGIC "PPF\x7f" +/* Forward declaration of opaque structure grub_font. + * Users only pass struct grub_font pointers to the font module functions, + * and do not have knowledge of the structure contents. */ +struct grub_font; + +/* Font type used to access font functions. */ +typedef struct grub_font *grub_font_t; + struct grub_font_glyph { - /* Glyph width in pixels. */ - grub_uint8_t width; - - /* Glyph height in pixels. */ - grub_uint8_t height; - - /* Glyph width in characters. */ - grub_uint8_t char_width; - - /* Glyph baseline position in pixels (from up). */ - grub_uint8_t baseline; - - /* Glyph bitmap data array of bytes in ((width + 7) / 8) * height. - Bitmap is formulated by height scanlines, each scanline having - width number of pixels. Pixels are coded as bits, value 1 meaning - of opaque pixel and 0 is transparent. If width does not fit byte - boundary, it will be padded with 0 to make it fit. */ - grub_uint8_t bitmap[32]; + /* Reference to the font this glyph belongs to. */ + grub_font_t font; + + /* Glyph bitmap width in pixels. */ + grub_uint16_t width; + + /* Glyph bitmap height in pixels. */ + grub_uint16_t height; + + /* Glyph bitmap x offset in pixels. Add to screen coordinate. */ + grub_int16_t offset_x; + + /* Glyph bitmap y offset in pixels. Subtract from screen coordinate. */ + grub_int16_t offset_y; + + /* Number of pixels to advance to start the next character. */ + grub_uint16_t device_width; + + /* Row-major order, packed bits (no padding; rows can break within a byte). + * The length of the array is (width * height + 7) / 8. Within a + * byte, the most significant bit is the first (leftmost/uppermost) pixel. + * Pixels are coded as bits, value 1 meaning of opaque pixel and 0 is + * transparent. If the length of the array does not fit byte boundary, it + * will be padded with 0 bits to make it fit. */ + grub_uint8_t bitmap[0]; }; -typedef struct grub_font_glyph *grub_font_glyph_t; - -int grub_font_get_glyph (grub_uint32_t code, - grub_font_glyph_t glyph); + +/****** font/font.c ******/ + +/* Get the font that has the specified name. Font names are in the form + * "Family Name Bold Italic 14", where Bold and Italic are optional. + * If no font matches the name specified, the most recently loaded font + * is returned as a fallback. */ +grub_font_t grub_font_get (const char *font_name); + +const char *grub_font_get_name (grub_font_t font); + +int grub_font_get_max_char_width (grub_font_t font); + +int grub_font_get_max_char_height (grub_font_t font); + +int grub_font_get_ascent (grub_font_t font); + +int grub_font_get_descent (grub_font_t font); + +int grub_font_get_string_width (grub_font_t font, const char *str); + + +/****** font/loader.c ******/ + +/* + * Load a font and add it to the beginning of the global font list. + * Returns: 0 upon success; nonzero upon failure. + */ +int grub_font_load (const char *filename); + +/* Get the glyph for FONT corresponding to the Unicode code point CODE. + * Returns a pointer to an glyph indicating there is no glyph available + * if CODE does not exist in the font. The glyphs are cached once loaded. */ +struct grub_font_glyph *grub_font_get_glyph (grub_font_t font, + grub_uint32_t code); + +/* Get a glyph corresponding to the codepoint CODE. If no glyph is available + * for CODE in the available fonts, then a glyph representing an unknown + * character is returned. This function never returns NULL. + * The returned glyph is owned by the font manager and should not be freed + * by the caller. The glyphs are cached. */ +struct grub_font_glyph *grub_font_get_glyph_any (grub_uint32_t code); #endif /* ! GRUB_FONT_HEADER */ === added file 'include/grub/font_internal.h' --- include/grub/font_internal.h 1970-01-01 00:00:00 +0000 +++ include/grub/font_internal.h 2008-07-03 14:16:11 +0000 @@ -0,0 +1,71 @@ +/* font_internal.h - Font declarations for use internally by the font module. + * Users of the font module should not include this header. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_FONT_INTERNAL_HEADER +#define GRUB_FONT_INTERNAL_HEADER 1 + +#include +#include +#include +#include +#include +#include +#include + +#define FONT_DEBUG 0 + +struct char_index_entry +{ + grub_uint32_t code; + grub_uint8_t storage_flags; + grub_uint32_t offset; + struct grub_font_glyph *glyph; /* Glyph if loaded, or null. */ +}; + +struct grub_font +{ + char *name; + grub_file_t file; + short max_char_width; + short max_char_height; + short ascent; + short descent; + grub_uint32_t num_chars; + struct char_index_entry *char_index; +}; + +struct font_node +{ + struct font_node *next; + struct grub_font *value; +}; + +extern struct font_node *grub_font_list; + + +/****** loader.c ******/ + +/* Initialize the font loader module. */ +void +grub_font_loader_init (void); + + +#endif /* ! GRUB_FONT_INTERNAL_HEADER */ + === added file 'include/grub/gfxmenu_model.h' --- include/grub/gfxmenu_model.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxmenu_model.h 2008-07-26 23:37:04 +0000 @@ -0,0 +1,59 @@ +/* gfxmenu_model.h - gfxmenu model interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXMENU_MODEL_HEADER +#define GRUB_GFXMENU_MODEL_HEADER 1 + +#include + +struct grub_gfxmenu_model; /* Forward declaration of opaque type. */ +typedef struct grub_gfxmenu_model *grub_gfxmenu_model_t; + + +grub_gfxmenu_model_t grub_gfxmenu_model_new (grub_menu_t menu); + +void grub_gfxmenu_model_destroy (grub_gfxmenu_model_t model); + +grub_menu_t grub_gfxmenu_model_get_menu (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_set_timeout (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_clear_timeout (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_timeout_ms (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_timeout_remaining_ms (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_timeout_expired (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_num_entries (grub_gfxmenu_model_t model); + +int grub_gfxmenu_model_get_selected_index (grub_gfxmenu_model_t model); + +void grub_gfxmenu_model_set_selected_index (grub_gfxmenu_model_t model, + int index); + +const char *grub_gfxmenu_model_get_entry_title (grub_gfxmenu_model_t model, + int index); + +grub_menu_entry_t grub_gfxmenu_model_get_entry (grub_gfxmenu_model_t model, + int index); + +#endif /* GRUB_GFXMENU_MODEL_HEADER */ + === added file 'include/grub/gfxmenu_view.h' --- include/grub/gfxmenu_view.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxmenu_view.h 2008-08-12 02:55:06 +0000 @@ -0,0 +1,108 @@ +/* gfxmenu_view.h - gfxmenu view interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXMENU_VIEW_HEADER +#define GRUB_GFXMENU_VIEW_HEADER 1 + +#include +#include +#include +#include + +struct grub_gfxmenu_view; /* Forward declaration of opaque type. */ +typedef struct grub_gfxmenu_view *grub_gfxmenu_view_t; + + +grub_gfxmenu_view_t grub_gfxmenu_view_new (const char *theme_path, + grub_gfxmenu_model_t model); + +void grub_gfxmenu_view_destroy (grub_gfxmenu_view_t view); + +/* Set properties on the view based on settings from the specified + theme file. */ +int grub_gfxmenu_view_load_theme (grub_gfxmenu_view_t view, + const char *theme_path); + +void grub_gfxmenu_view_draw (grub_gfxmenu_view_t view); + +int grub_gfxmenu_view_execute_with_fallback (grub_gfxmenu_view_t view, + grub_menu_entry_t entry); + +int grub_gfxmenu_view_execute_entry (grub_gfxmenu_view_t view, + grub_menu_entry_t entry); + +void grub_gfxmenu_view_run_terminal (grub_gfxmenu_view_t view); + + + +/* Implementation details -- this should not be used outside of the + view itself. */ + +#include +#include +#include +#include + +typedef struct grub_gfxmenu_view_icon +{ + const char *class_name; + struct grub_video_bitmap *bitmap; + struct grub_gfxmenu_view_icon *next; +} *grub_gfxmenu_view_icon_t; + +/* Definition of the private representation of the view. */ +struct grub_gfxmenu_view +{ + grub_video_rect_t screen; + + int icon_width; + int icon_height; + int item_height; + int item_padding; + int item_icon_space; + int item_spacing; + grub_font_t title_font; + grub_font_t item_font; + grub_font_t selected_item_font; + grub_font_t status_font; + char *terminal_font_name; + grub_gui_color_t title_color; + grub_gui_color_t item_color; + grub_gui_color_t selected_item_color; + grub_gui_color_t status_color; + grub_gui_color_t status_bg_color; + struct grub_video_bitmap *desktop_image; + grub_gui_color_t desktop_color; + grub_gfxmenu_box_t menu_box; + grub_gfxmenu_box_t selected_item_box; + grub_gfxmenu_box_t terminal_box; + char *title_text; + char *progress_message_text; + char *theme_path; + /* Icon cache: linked list w/ dummy head node. */ + struct grub_gfxmenu_view_icon icon_cache; + + grub_gui_container_t canvas; + + grub_gfxmenu_model_t model; +}; + +void grub_gfxmenu_view_free_icon_cache (grub_gfxmenu_view_t view); + +#endif /* ! GRUB_GFXMENU_VIEW_HEADER */ === added file 'include/grub/gfxterm.h' --- include/grub/gfxterm.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxterm.h 2008-07-19 19:56:06 +0000 @@ -0,0 +1,41 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXTERM_HEADER +#define GRUB_GFXTERM_HEADER 1 + +#include +#include +#include +#include + +grub_err_t +grub_gfxterm_init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width); + +void grub_gfxterm_destroy_window (void); + +grub_term_t grub_gfxterm_get_term (void); + +typedef void (*grub_gfxterm_repaint_callback_t)(int x, int y, + int width, int height); + +void grub_gfxterm_set_repaint_callback (grub_gfxterm_repaint_callback_t func); + +#endif /* ! GRUB_GFXTERM_HEADER */ === added file 'include/grub/gfxwidgets.h' --- include/grub/gfxwidgets.h 1970-01-01 00:00:00 +0000 +++ include/grub/gfxwidgets.h 2008-07-19 18:25:18 +0000 @@ -0,0 +1,47 @@ +/* gfxwidgets.h - Widgets for the graphical menu (gfxmenu). */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXWIDGETS_HEADER +#define GRUB_GFXWIDGETS_HEADER 1 + +#include + +typedef struct grub_gfxmenu_box *grub_gfxmenu_box_t; + +struct grub_gfxmenu_box +{ + /* The size of the content. */ + int width; + int height; + + struct grub_video_bitmap **raw_pixmaps; + struct grub_video_bitmap **scaled_pixmaps; + + void (*draw) (grub_gfxmenu_box_t self, int x, int y); + void (*set_content_size) (grub_gfxmenu_box_t self, + int width, int height); + int (*get_left_pad) (grub_gfxmenu_box_t self); + int (*get_top_pad) (grub_gfxmenu_box_t self); + void (*destroy) (grub_gfxmenu_box_t self); +}; + +grub_gfxmenu_box_t grub_gfxmenu_create_box (const char *pixmaps_prefix, + const char *pixmaps_suffix); + +#endif /* ! GRUB_GFXWIDGETS_HEADER */ === added file 'include/grub/gui.h' --- include/grub/gui.h 1970-01-01 00:00:00 +0000 +++ include/grub/gui.h 2008-08-11 14:36:33 +0000 @@ -0,0 +1,144 @@ +/* gui.h - GUI components header file. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +#ifndef GRUB_GUI_H +#define GRUB_GUI_H 1 + +/* A representation of a color. Unlike grub_video_color_t, this + representation is independent of any video mode specifics. */ +typedef struct grub_gui_color +{ + grub_uint8_t red; + grub_uint8_t green; + grub_uint8_t blue; + grub_uint8_t alpha; +} grub_gui_color_t; + +typedef struct grub_gui_component *grub_gui_component_t; +typedef struct grub_gui_container *grub_gui_container_t; + +typedef void (*grub_gui_component_callback) (grub_gui_component_t component, + void *userdata); + +/* Component interface. */ + +struct grub_gui_component_ops +{ + void (*destroy) (void *self); + const char * (*get_id) (void *self); + int (*is_instance) (void *self, const char *type); + void (*paint) (void *self); + void (*set_parent) (void *self, grub_gui_container_t parent); + grub_gui_container_t (*get_parent) (void *self); + void (*set_bounds) (void *self, const grub_video_rect_t *bounds); + void (*get_bounds) (void *self, grub_video_rect_t *bounds); + void (*get_preferred_size) (void *self, int *width, int *height); + void (*set_property) (void *self, const char *name, const char *value); +}; + +struct grub_gui_container_ops +{ + struct grub_gui_component_ops component; + void (*add) (void *self, grub_gui_component_t comp); + void (*remove) (void *self, grub_gui_component_t comp); + void (*iterate_children) (void *self, + grub_gui_component_callback cb, void *userdata); +}; + +struct grub_gui_component +{ + struct grub_gui_component_ops *ops; +}; + +struct grub_gui_container +{ + struct grub_gui_container_ops *ops; +}; + + +/* Interfaces to concrete component classes. */ + +grub_gui_container_t grub_gui_canvas_new (void); +grub_gui_container_t grub_gui_vbox_new (void); +grub_gui_container_t grub_gui_hbox_new (void); +grub_gui_component_t grub_gui_label_new (void); +grub_gui_component_t grub_gui_image_new (void); +grub_gui_component_t grub_gui_progress_bar_new (void); + +/* Manipulation functions. */ + +/* Visit all components with the specified ID. */ +void grub_gui_find_by_id (grub_gui_component_t root, + const char *id, + grub_gui_component_callback cb, + void *userdata); + +/* Helper functions. */ + +static __inline void +grub_gui_save_viewport (grub_video_rect_t *r) +{ + grub_video_get_viewport ((unsigned *) &r->x, + (unsigned *) &r->y, + (unsigned *) &r->width, + (unsigned *) &r->height); +} + +static __inline void +grub_gui_restore_viewport (const grub_video_rect_t *r) +{ + grub_video_set_viewport (r->x, r->y, r->width, r->height); +} + +/* Set a new viewport relative the the current one, saving the current + viewport in OLD so it can be later restored. */ +static __inline void +grub_gui_set_viewport (const grub_video_rect_t *r, grub_video_rect_t *old) +{ + grub_gui_save_viewport (old); + grub_video_set_viewport (old->x + r->x, + old->y + r->y, + r->width, + r->height); +} + +static __inline grub_gui_color_t +grub_gui_color_rgb (int r, int g, int b) +{ + grub_gui_color_t c; + c.red = r; + c.green = g; + c.blue = b; + c.alpha = 255; + return c; +} + +static __inline grub_video_color_t +grub_gui_map_color (grub_gui_color_t c) +{ + return grub_video_map_rgba (c.red, c.green, c.blue, c.alpha); +} + + +#endif /* ! GRUB_GUI_H */ === modified file 'include/grub/i386/pc/vbeblit.h' --- include/grub/i386/pc/vbeblit.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/pc/vbeblit.h 2008-07-09 17:27:53 +0000 @@ -25,55 +25,93 @@ struct grub_video_i386_vbeblit_info; void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8X8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_index_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_index_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_R8G8B8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y); - -void -grub_video_i386_vbeblit_index_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_BGRX8888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGRA8888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGR888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGRA8888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGR888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_BGR888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGBA8888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +/* Direct copy for compatible 32 bpp blit formats. + * (RGBA8888->RGBA8888, BGRA8888->BGRA8888, etc.) */ +void +grub_video_i386_vbeblit_direct32_copy (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGB888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGB888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_index_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_index_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGBA8888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_RGB888_RGB888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y); + +void +grub_video_i386_vbeblit_index_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y); === modified file 'include/grub/i386/pc/vbefill.h' --- include/grub/i386/pc/vbefill.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/pc/vbefill.h 2008-07-03 13:49:18 +0000 @@ -25,14 +25,14 @@ struct grub_video_i386_vbeblit_info; void -grub_video_i386_vbefill_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbefill_direct32 (struct grub_video_i386_vbeblit_info *dst, grub_video_color_t color, int x, int y, int width, int height); void -grub_video_i386_vbefill_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - grub_video_color_t color, int x, int y, - int width, int height); +grub_video_i386_vbefill_direct24 (struct grub_video_i386_vbeblit_info *dst, + grub_video_color_t color, int x, int y, + int width, int height); void grub_video_i386_vbefill_index (struct grub_video_i386_vbeblit_info *dst, === modified file 'include/grub/i386/types.h' --- include/grub/i386/types.h 2007-07-21 22:32:33 +0000 +++ include/grub/i386/types.h 2008-08-11 14:36:33 +0000 @@ -25,6 +25,51 @@ /* The size of long. */ #define GRUB_TARGET_SIZEOF_LONG 4 +/* The native word size in bits. */ +#define __WORDSIZE 32 + +/* These assume 8-bit `char's, 16-bit `short int's, + and 32-bit `int's and `long int's. */ + +/* Number of bits in a `char'. */ +#define CHAR_BIT 8 + +/* Minimum and maximum values a `signed char' can hold. */ +#define SCHAR_MIN (-128) +#define SCHAR_MAX 127 + +/* Maximum value an `unsigned char' can hold. (Minimum is 0.) */ +#define UCHAR_MAX 255 + +/* Minimum and maximum values a `char' can hold. */ +#ifdef __CHAR_UNSIGNED__ +# define CHAR_MIN 0 +# define CHAR_MAX UCHAR_MAX +#else +# define CHAR_MIN SCHAR_MIN +# define CHAR_MAX SCHAR_MAX +#endif + +/* Minimum and maximum values for signed short int. */ +#define SHRT_MIN (-32768) +#define SHRT_MAX 32767 + +/* Maximum value for unsigned short int. (Minimum is 0.) */ +#define USHRT_MAX 65535 + +/* Mininum and maximum values for signed int. */ +#define INT_MIN (-INT_MAX - 1) +#define INT_MAX 2147483647 +#define UINT_MAX 4294967295U + +/* Mininum and maximum values for signed long. */ +#if __WORDSIZE == 64 +# define LONG_MAX 9223372036854775807L +#else +# define LONG_MAX 2147483647L +#endif +#define LONG_MIN (-LONG_MAX - 1L) + /* i386 is little-endian. */ #undef GRUB_TARGET_WORDS_BIGENDIAN === added file 'include/grub/menu.h' --- include/grub/menu.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu.h 2008-08-03 02:14:40 +0000 @@ -0,0 +1,63 @@ +/* menu.h - Menu and menu entry model declarations. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_MENU_HEADER +#define GRUB_MENU_HEADER 1 + +struct grub_menu_entry_class +{ + char *name; + struct grub_menu_entry_class *next; +}; + +/* The menu entry. */ +struct grub_menu_entry +{ + /* The title name. */ + const char *title; + + /* The classes associated with the menu entry: + used to choose an icon or other style attributes. + This is a dummy head node for the linked list, so for an entry E, + E.classes->next is the first class if it is not NULL. */ + struct grub_menu_entry_class *classes; + + /* The commands associated with this menu entry. */ + struct grub_script *commands; + + /* The sourcecode of the menu entry, used by the editor. */ + const char *sourcecode; + + /* The next element. */ + struct grub_menu_entry *next; +}; +typedef struct grub_menu_entry *grub_menu_entry_t; + +/* The menu. */ +struct grub_menu +{ + /* The size of a menu. */ + int size; + + /* The list of menu entries. */ + grub_menu_entry_t entry_list; +}; +typedef struct grub_menu *grub_menu_t; + +#endif /* GRUB_MENU_HEADER */ === added file 'include/grub/menu_viewer.h' --- include/grub/menu_viewer.h 1970-01-01 00:00:00 +0000 +++ include/grub/menu_viewer.h 2008-07-19 19:07:47 +0000 @@ -0,0 +1,48 @@ +/* menu_viewer.h - Interface to menu viewer implementations. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_MENU_VIEWER_HEADER +#define GRUB_MENU_VIEWER_HEADER 1 + +#include +#include +#include +#include + +struct grub_menu_viewer +{ + /* The menu viewer name. */ + const char *name; + + grub_err_t (*show_menu) (grub_menu_t menu, int nested); + + struct grub_menu_viewer *next; +}; +typedef struct grub_menu_viewer *grub_menu_viewer_t; + +void EXPORT_FUNC(grub_menu_viewer_init) (void); + +void EXPORT_FUNC(grub_menu_viewer_register) (grub_menu_viewer_t viewer); + +grub_err_t EXPORT_FUNC(grub_menu_viewer_show_menu) (grub_menu_t menu, int nested); + +/* Return nonzero iff the menu viewer should clean up and return ASAP. */ +int EXPORT_FUNC(grub_menu_viewer_should_return) (void); + +#endif /* GRUB_MENU_VIEWER_HEADER */ === modified file 'include/grub/misc.h' --- include/grub/misc.h 2008-07-04 01:12:54 +0000 +++ include/grub/misc.h 2008-08-11 14:36:33 +0000 @@ -52,11 +52,14 @@ char *EXPORT_FUNC(grub_strstr) (const char *haystack, const char *needle); int EXPORT_FUNC(grub_iswordseparator) (int c); int EXPORT_FUNC(grub_isspace) (int c); +int EXPORT_FUNC(grub_iscntrl) (int c); int EXPORT_FUNC(grub_isprint) (int c); int EXPORT_FUNC(grub_isalpha) (int c); +int EXPORT_FUNC(grub_isalnum) (int c); int EXPORT_FUNC(grub_isgraph) (int c); int EXPORT_FUNC(grub_isdigit) (int c); int EXPORT_FUNC(grub_tolower) (int c); +long EXPORT_FUNC(grub_strtol) (const char *str, char **end, int base); unsigned long EXPORT_FUNC(grub_strtoul) (const char *str, char **end, int base); unsigned long long EXPORT_FUNC(grub_strtoull) (const char *str, char **end, int base); char *EXPORT_FUNC(grub_strdup) (const char *s); === modified file 'include/grub/normal.h' --- include/grub/normal.h 2008-03-26 12:01:02 +0000 +++ include/grub/normal.h 2008-08-03 04:00:22 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include /* The maximum size of a command-line. */ #define GRUB_MAX_CMDLINE 1600 @@ -39,7 +40,7 @@ #define GRUB_COMMAND_FLAG_TITLE 0x4 /* Don't print the command on booting. */ #define GRUB_COMMAND_FLAG_NO_ECHO 0x8 -/* Don't print the command on booting. */ +/* Pass arguments to the command without parsing options. */ #define GRUB_COMMAND_FLAG_NO_ARG_PARSE 0x10 /* Not loaded yet. Used for auto-loading. */ #define GRUB_COMMAND_FLAG_NOT_LOADED 0x20 @@ -84,34 +85,6 @@ }; typedef struct grub_command *grub_command_t; -/* The menu entry. */ -struct grub_menu_entry -{ - /* The title name. */ - const char *title; - - /* The commands associated with this menu entry. */ - struct grub_script *commands; - - /* The sourcecode of the menu entry, used by the editor. */ - const char *sourcecode; - - /* The next element. */ - struct grub_menu_entry *next; -}; -typedef struct grub_menu_entry *grub_menu_entry_t; - -/* The menu. */ -struct grub_menu -{ - /* The size of a menu. */ - int size; - - /* The list of menu entries. */ - grub_menu_entry_t entry_list; -}; -typedef struct grub_menu *grub_menu_t; - /* This is used to store the names of filesystem modules for auto-loading. */ struct grub_fs_module_list { @@ -123,10 +96,27 @@ /* To exit from the normal mode. */ extern grub_jmp_buf grub_exit_env; +extern struct grub_menu_viewer grub_normal_terminal_menu_viewer; + +typedef struct grub_menu_execute_callback +{ + void (*notify_booting) (void *userdata, grub_menu_entry_t entry); + void (*notify_fallback) (void *userdata, grub_menu_entry_t entry); + void (*notify_failure) (void *userdata); +} +*grub_menu_execute_callback_t; + void grub_enter_normal_mode (const char *config); void grub_normal_execute (const char *config, int nested); -void grub_menu_run (grub_menu_t menu, int nested); +void grub_menu_execute_with_fallback (grub_menu_t menu, + grub_menu_entry_t entry, + grub_menu_execute_callback_t callback, + void *callback_data); void grub_menu_entry_run (grub_menu_entry_t entry); +int grub_menu_get_default_entry_index (grub_menu_t menu); +int grub_menu_get_timeout (void); +void grub_menu_set_timeout (int timeout); +void grub_menu_execute_entry(grub_menu_entry_t entry); void grub_cmdline_run (int nested); int grub_cmdline_get (const char *prompt, char cmdline[], unsigned max_len, int echo_char, int readline); === added file 'include/grub/stringutil.h' --- include/grub/stringutil.h 1970-01-01 00:00:00 +0000 +++ include/grub/stringutil.h 2008-08-10 04:38:24 +0000 @@ -0,0 +1,37 @@ +/* gfxmenu_stringutil.h - String utilities for the graphical menu interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#ifndef GRUB_GFXMENU_STRINGUTIL_HEADER +#define GRUB_GFXMENU_STRINGUTIL_HEADER 1 + +#include +#include + +char *grub_new_substring (const char *buf, + grub_size_t start, grub_size_t end); + +char *grub_resolve_relative_path (const char *base, const char *path); + +char *grub_get_dirname (const char *file_path); + +grub_gui_color_t grub_gui_parse_color (const char *s); + +int grub_gui_parse_2_tuple (const char *s, int *px, int *py); + +#endif /* GRUB_GFXMENU_STRINGUTIL_HEADER */ === modified file 'include/grub/video.h' --- include/grub/video.h 2008-01-01 12:02:07 +0000 +++ include/grub/video.h 2008-07-19 19:31:46 +0000 @@ -21,6 +21,7 @@ #include #include +#include /* Video color in hardware dependent format. Users should not assume any specific coding format. */ @@ -31,40 +32,41 @@ struct grub_video_render_target; /* Forward declarations for used data structures. */ -struct grub_font_glyph; struct grub_video_bitmap; /* Defines used to describe video mode or rendering target. */ -#define GRUB_VIDEO_MODE_TYPE_ALPHA 0x00000008 -#define GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED 0x00000004 +#define GRUB_VIDEO_MODE_TYPE_ALPHA 0x00000020 +#define GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED 0x00000010 +#define GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP 0x00000004 #define GRUB_VIDEO_MODE_TYPE_INDEX_COLOR 0x00000002 #define GRUB_VIDEO_MODE_TYPE_RGB 0x00000001 /* Defines used to mask flags. */ -#define GRUB_VIDEO_MODE_TYPE_COLOR_MASK 0x00000003 +#define GRUB_VIDEO_MODE_TYPE_COLOR_MASK 0x0000000F /* Defines used to specify requested bit depth. */ #define GRUB_VIDEO_MODE_TYPE_DEPTH_MASK 0x0000ff00 #define GRUB_VIDEO_MODE_TYPE_DEPTH_POS 8 -/* Defined predefined render targets. */ -#define GRUB_VIDEO_RENDER_TARGET_DISPLAY ((struct grub_video_render_target *) 0) -#define GRUB_VIDEO_RENDER_TARGET_FRONT_BUFFER ((struct grub_video_render_target *) 0) -#define GRUB_VIDEO_RENDER_TARGET_BACK_BUFFER ((struct grub_video_render_target *) 1) +/* Predefined render target: */ +/* The render target that client code should render to. */ +#define GRUB_VIDEO_RENDER_TARGET_DISPLAY \ + ((struct grub_video_render_target *) 0) + /* Defined blitting formats. */ enum grub_video_blit_format { - /* Follow exactly field & mask information. */ - GRUB_VIDEO_BLIT_FORMAT_RGBA, - /* Make optimization assumption. */ - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8, - /* Follow exactly field & mask information. */ - GRUB_VIDEO_BLIT_FORMAT_RGB, - /* Make optimization assumption. */ - GRUB_VIDEO_BLIT_FORMAT_R8G8B8, + GRUB_VIDEO_BLIT_FORMAT_RGBA, /* General: Use fields & masks. */ + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_BGRA_8888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_RGB, /* General: Use fields & masks. */ + GRUB_VIDEO_BLIT_FORMAT_RGB_888, /* Optimized format. */ + GRUB_VIDEO_BLIT_FORMAT_BGR_888, /* Optimized format. */ /* When needed, decode color or just use value as is. */ - GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR + GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR, + /* Two color bitmap; bits packed: rows are not padded to byte boundary. */ + GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED }; /* Define blitting operators. */ @@ -127,6 +129,18 @@ /* What is location of reserved color bits. In Index Color mode, this is 0. */ unsigned int reserved_field_pos; + + /* For 1-bit bitmaps, the background color. Used for bits = 0. */ + grub_uint8_t bg_red; + grub_uint8_t bg_green; + grub_uint8_t bg_blue; + grub_uint8_t bg_alpha; + + /* For 1-bit bitmaps, the foreground color. Used for bits = 1. */ + grub_uint8_t fg_red; + grub_uint8_t fg_green; + grub_uint8_t fg_blue; + grub_uint8_t fg_alpha; }; struct grub_video_palette_data @@ -137,6 +151,16 @@ grub_uint8_t a; /* Reserved bits value (0-255). */ }; +/* A 2D rectangle type. */ +struct grub_video_rect +{ + int x; + int y; + int width; + int height; +}; +typedef struct grub_video_rect grub_video_rect_t; + struct grub_video_adapter { /* The video adapter name. */ @@ -183,6 +207,10 @@ grub_err_t (*blit_glyph) (struct grub_font_glyph *glyph, grub_video_color_t color, int x, int y); + grub_err_t (*draw_string) (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y); + grub_err_t (*blit_bitmap) (struct grub_video_bitmap *bitmap, enum grub_video_blit_operators oper, int x, int y, int offset_x, int offset_y, @@ -255,6 +283,10 @@ grub_err_t grub_video_blit_glyph (struct grub_font_glyph *glyph, grub_video_color_t color, int x, int y); +grub_err_t grub_video_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y); + grub_err_t grub_video_blit_bitmap (struct grub_video_bitmap *bitmap, enum grub_video_blit_operators oper, int x, int y, int offset_x, int offset_y, @@ -269,6 +301,9 @@ grub_err_t grub_video_scroll (grub_video_color_t color, int dx, int dy); +/* Swap the pages referred to by the front buffer and back buffer render + * targets, and the page previously referred to by the back buffer is made + * visible on the display. */ grub_err_t grub_video_swap_buffers (void); grub_err_t grub_video_create_render_target (struct grub_video_render_target **result, @@ -282,4 +317,21 @@ grub_err_t grub_video_get_active_render_target (struct grub_video_render_target **target); + +/* Defined in video/setmode.c */ + +/* Set the video mode based on the preferred modes specified in MODE_LIST in + the form: x[x][;...] + + For example: 640x480;800x600x8;400x300x32 + + If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and + DEFAULT_HEIGHT are used to set the mode. The MODE_FLAGS argument determines + the video mode flags such as double buffering that are used. */ + +grub_err_t +grub_video_setup_preferred_mode (const char *mode_list, int mode_flags, + int default_width, int default_height); + + #endif /* ! GRUB_VIDEO_HEADER */ === added file 'kern/menu_viewer.c' --- kern/menu_viewer.c 1970-01-01 00:00:00 +0000 +++ kern/menu_viewer.c 2008-07-19 19:07:47 +0000 @@ -0,0 +1,99 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2002,2003,2005,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include + +/* The list of menu viewers. */ +static grub_menu_viewer_t menu_viewer_list; + +static int should_return; +static int menu_viewer_changed; + +void +grub_menu_viewer_register (grub_menu_viewer_t viewer) +{ + viewer->next = menu_viewer_list; + menu_viewer_list = viewer; +} + +static grub_menu_viewer_t get_current_menu_viewer (void) +{ + const char *selected_name = grub_env_get ("menuviewer"); + + /* If none selected, pick the last registered one. */ + if (selected_name == 0) + return menu_viewer_list; + + grub_menu_viewer_t cur; + for (cur = menu_viewer_list; cur; cur = cur->next) + { + if (grub_strcmp (cur->name, selected_name) == 0) + return cur; + } + + /* Fall back to the first entry (or null). */ + return menu_viewer_list; +} + +grub_err_t +grub_menu_viewer_show_menu (grub_menu_t menu, int nested) +{ + grub_err_t err; + int repeat = 0; + do + { + repeat = 0; + menu_viewer_changed = 0; + grub_menu_viewer_t cur = get_current_menu_viewer (); + if (!cur) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "No menu viewer available."); + + should_return = 0; + err = cur->show_menu (menu, nested); + if (menu_viewer_changed) + repeat = 1; + } + while (repeat); + return err; +} + +int +grub_menu_viewer_should_return (void) +{ + return should_return; +} + +static char * +menuviewer_write_hook (struct grub_env_var *var __attribute__ ((unused)), + const char *val) +{ + menu_viewer_changed = 1; + should_return = 1; + return grub_strdup (val); +} + +void +grub_menu_viewer_init (void) +{ + grub_register_variable_hook ("menuviewer", 0, menuviewer_write_hook); +} + === modified file 'kern/misc.c' --- kern/misc.c 2008-08-05 10:54:37 +0000 +++ kern/misc.c 2008-08-11 14:36:33 +0000 @@ -358,6 +358,12 @@ } int +grub_iscntrl (int c) +{ + return (c >= 0x00 && c <= 0x1F) || c == 0x7F; +} + +int grub_isprint (int c) { return (c >= ' ' && c <= '~'); @@ -370,6 +376,12 @@ } int +grub_isalnum (int c) +{ + return grub_isalpha (c) || grub_isdigit (c); +} + +int grub_isdigit (int c) { return (c >= '0' && c <= '9'); @@ -390,6 +402,41 @@ return c; } +long +grub_strtol (const char *str, char **end, int base) +{ + int negative = 0; + + while (*str && grub_isspace (*str)) + str++; + + if (*str == '-') + { + negative = 1; + str++; + } + + unsigned long long magnitude; + magnitude = grub_strtoull (str, end, base); + if (negative) + { + if (magnitude > -((long long) LONG_MIN)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "negative overflow"); + return LONG_MIN; + } + return -((long long) magnitude); + } + else + { + if (magnitude > LONG_MAX) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "positive overflow"); + return LONG_MAX; + } + return (long) magnitude; + } +} unsigned long grub_strtoul (const char *str, char **end, int base) === modified file 'normal/main.c' --- normal/main.c 2008-07-29 14:07:47 +0000 +++ normal/main.c 2008-08-03 04:00:22 +0000 @@ -28,6 +28,7 @@ #include #include #include +#include grub_jmp_buf grub_exit_env; @@ -147,14 +148,132 @@ grub_env_unset_data_slot ("menu"); } +static void +free_menu_entry_classes (struct grub_menu_entry_class *head) +{ + /* Free all the classes. */ + while (head) + { + struct grub_menu_entry_class *next; + + grub_free (head->name); + next = head->next; + grub_free (head); + head = next; + } +} + +/* The tag that can be added to a menu entry's title to specify a class + for the UI to use in selecting an icon or other visual attributes. */ +static const char entry_class_attr_tag[] = "|class="; + +#define ENTRY_ATTR_SEPARATOR_CHAR ',' + +/* Parse and strip a possible "class" attribute in the title. + This code is not designed to support other attributes than "class" + in the title since, we are planning to use a better method of + specifying this information in the future. The parameter TITLE is + modified by storing a '\0' at the appropriate location to strip the + class information, if it exists. The class list is stored into *HEAD. */ +static struct grub_menu_entry_class * +get_classes_from_entry_title (char *title) +{ + struct grub_menu_entry_class *head; + char *attr_start; + + head = grub_malloc (sizeof (struct grub_menu_entry_class)); + if (! head) + return 0; + head->name = 0; + head->next = 0; + + attr_start = grub_strstr (title, entry_class_attr_tag); + if (attr_start) + { + struct grub_menu_entry_class *tail; + const char *p; + + /* Trim the properties off of the title. */ + *attr_start = '\0'; + + /* Move the pointer to the beginning of the first class name. */ + attr_start += grub_strlen (entry_class_attr_tag); + + tail = head; + p = attr_start; + while (p && *p) + { + const char *q; + const char *end; + const char *next_start; + + /* Skip any leading whitespace. */ + while (*p && grub_isspace (*p)) + p++; + + /* Find the comma terminating this one ... */ + q = grub_strchr (p, ENTRY_ATTR_SEPARATOR_CHAR); + /* ... or if it's the last one, find the '\0' terminator. */ + if (q) + { + end = q - 1; + next_start = q + 1; + } + else + { + /* For the last class, extend it to the end. */ + end = p + grub_strlen (p); + next_start = 0; + } + + /* Trim any trailing whitespace. */ + while (end > p && grub_isspace (*end)) + end--; + + grub_size_t len = end - p + 1; + /* Copy the class name into a new string. */ + char *class_name = grub_malloc (len + 1); + if (! class_name) + { + free_menu_entry_classes (head); + return 0; + } + grub_memcpy (class_name, p, len); + class_name[len] = '\0'; + + /* Create a new class and add it at the tail of the list. */ + struct grub_menu_entry_class *new_class; + new_class = grub_malloc (sizeof (struct grub_menu_entry_class)); + if (! new_class) + { + grub_free (class_name); + free_menu_entry_classes (head); + return 0; + } + /* Fill in the new class node. */ + new_class->name = class_name; + new_class->next = 0; + /* Link the tail to it, and make it the new tail. */ + tail->next = new_class; + tail = new_class; + + /* Advance the character pointer. */ + p = next_start; + } + } + + return head; +} + grub_err_t grub_normal_menu_addentry (const char *title, struct grub_script *script, const char *sourcecode) { - const char *menutitle; + char *menutitle; const char *menusourcecode; grub_menu_t menu; grub_menu_entry_t *last; + struct grub_menu_entry_class *classes; menu = grub_env_get_data_slot("menu"); if (! menu) @@ -173,6 +292,14 @@ return grub_errno; } + classes = get_classes_from_entry_title (menutitle); + if (! classes) + { + grub_free ((void *) menutitle); + grub_free ((void *) menusourcecode); + return grub_errno; + } + /* Add the menu entry at the end of the list. */ while (*last) last = &(*last)->next; @@ -180,6 +307,7 @@ *last = grub_malloc (sizeof (**last)); if (! *last) { + free_menu_entry_classes (classes); grub_free ((void *) menutitle); grub_free ((void *) menusourcecode); return grub_errno; @@ -187,6 +315,7 @@ (*last)->commands = script; (*last)->title = menutitle; + (*last)->classes = classes; (*last)->next = 0; (*last)->sourcecode = menusourcecode; @@ -476,7 +605,7 @@ if (menu && menu->size) { - grub_menu_run (menu, nested); + grub_menu_viewer_show_menu (menu, nested); if (nested) free_menu (menu); } @@ -519,6 +648,8 @@ if (mod) grub_dl_ref (mod); + grub_menu_viewer_register (&grub_normal_terminal_menu_viewer); + grub_set_history (GRUB_DEFAULT_HISTORY_SIZE); /* Register a command "normal" for the rescue mode. */ @@ -535,6 +666,8 @@ /* This registers some built-in commands. */ grub_command_init (); + + grub_menu_viewer_init (); } GRUB_MOD_FINI(normal) === modified file 'normal/menu.c' --- normal/menu.c 2008-08-07 22:48:13 +0000 +++ normal/menu.c 2008-08-11 14:46:59 +0000 @@ -24,6 +24,7 @@ #include #include #include +#include static grub_uint8_t grub_color_menu_normal; static grub_uint8_t grub_color_menu_highlight; @@ -241,8 +242,8 @@ /* Return the current timeout. If the variable "timeout" is not set or invalid, return -1. */ -static int -get_timeout (void) +int +grub_menu_get_timeout (void) { char *val; int timeout; @@ -269,8 +270,8 @@ } /* Set current timeout in the variable "timeout". */ -static void -set_timeout (int timeout) +void +grub_menu_set_timeout (int timeout) { /* Ignore TIMEOUT if it is zero, because it will be unset really soon. */ if (timeout > 0) @@ -308,6 +309,57 @@ return entry; } +/* Get the default menu entry index. */ +int +grub_menu_get_default_entry_index (grub_menu_t menu) +{ + int i = get_entry_number ("default"); + + /* If DEFAULT_ENTRY is not within the menu entries, fall back to + the first entry. */ + if (i < 0 || i >= menu->size) + i = 0; + + return i; +} + +/* Get the first entry number from the variable NAME, which is a + space-separated list of nonnegative integers. The entry number which + is returned is stripped from the value of NAME. */ +static int +get_and_remove_first_entry_number (const char *name) +{ + char *val; + char *tail; + int entry; + + val = grub_env_get (name); + if (! val) + return -1; + + grub_error_push (); + + entry = (int) grub_strtoul (val, &tail, 0); + + if (grub_errno == GRUB_ERR_NONE) + { + /* Skip whitespace to find the next digit. */ + while (*tail && grub_isspace (*tail)) + tail++; + grub_env_set (name, tail); + } + else + { + grub_env_unset (name); + grub_errno = GRUB_ERR_NONE; + entry = -1; + } + + grub_error_pop (); + + return entry; +} + static void print_timeout (int timeout, int offset, int second_stage) { @@ -332,15 +384,10 @@ first = 0; - default_entry = get_entry_number ("default"); - - /* If DEFAULT_ENTRY is not within the menu entries, fall back to - the first entry. */ - if (default_entry < 0 || default_entry >= menu->size) - default_entry = 0; + default_entry = grub_menu_get_default_entry_index (menu); /* If timeout is 0, drawing is pointless (and ugly). */ - if (get_timeout () == 0) + if (grub_menu_get_timeout () == 0) return default_entry; offset = default_entry; @@ -359,15 +406,15 @@ print_entries (menu, first, offset); grub_refresh (); - timeout = get_timeout (); + timeout = grub_menu_get_timeout (); if (timeout > 0) print_timeout (timeout, offset, 0); - while (1) + while (!grub_menu_viewer_should_return ()) { int c; - timeout = get_timeout (); + timeout = grub_menu_get_timeout (); if (timeout > 0) { @@ -377,7 +424,7 @@ if (current_time - saved_time >= 1000) { timeout--; - set_timeout (timeout); + grub_menu_set_timeout (timeout); saved_time = current_time; print_timeout (timeout, offset, 1); } @@ -468,6 +515,10 @@ } goto refresh; + case 't': + grub_env_set ("menuviewer", "protomenu"); + goto refresh; + default: break; } @@ -476,13 +527,14 @@ } } - /* Never reach here. */ + /* Exit menu without activating an item. This occurs if the user presses + * 't', switching to the graphical menu viewer. */ return -1; } /* Run a menu entry. */ -static void -run_menu_entry (grub_menu_entry_t entry) +void +grub_menu_execute_entry(grub_menu_entry_t entry) { grub_script_execute (entry->commands); @@ -491,15 +543,80 @@ grub_command_execute ("boot", 0); } +/* Execute ENTRY from the menu MENU, falling back to entries specified + in the environment variable "fallback" if it fails. CALLBACK is a + pointer to a struct of function pointers which are used to allow the + caller provide feedback to the user. */ void -grub_menu_run (grub_menu_t menu, int nested) +grub_menu_execute_with_fallback (grub_menu_t menu, + grub_menu_entry_t entry, + grub_menu_execute_callback_t callback, + void *callback_data) +{ + int fallback_entry; + + callback->notify_booting (callback_data, entry); + + grub_menu_execute_entry (entry); + + /* Deal with fallback entries. */ + while ((fallback_entry = get_and_remove_first_entry_number ("fallback")) + >= 0) + { + grub_print_error (); + grub_errno = GRUB_ERR_NONE; + + entry = get_entry (menu, fallback_entry); + callback->notify_fallback (callback_data, entry); + grub_menu_execute_entry (entry); + } + + if (grub_errno != GRUB_ERR_NONE) + callback->notify_failure (callback_data); +} + +static void +notify_booting (void *userdata __attribute__((unused)), + grub_menu_entry_t entry) +{ + grub_printf (" Booting \'%s\'\n\n", entry->title); +} + +static void +notify_fallback (void *userdata __attribute__((unused)), + grub_menu_entry_t entry) +{ + grub_printf ("\n Falling back to \'%s\'\n\n", entry->title); + grub_millisleep (2000); +} + +static void +notify_execution_failure (void *userdata __attribute__((unused))) +{ + if (grub_errno != GRUB_ERR_NONE) + { + grub_print_error (); + grub_errno = GRUB_ERR_NONE; + + grub_wait_after_message (); + } +} + +static struct grub_menu_execute_callback execution_callback = +{ + .notify_booting = notify_booting, + .notify_fallback = notify_fallback, + .notify_failure = notify_execution_failure +}; + +static grub_err_t +show_menu (grub_menu_t menu, int nested) { while (1) { int boot_entry; grub_menu_entry_t e; - int fallback_entry; - + boot_entry = run_menu (menu, nested); if (boot_entry < 0) break; @@ -507,34 +624,18 @@ e = get_entry (menu, boot_entry); if (! e) continue; /* Menu is empty. */ - + grub_cls (); grub_setcursor (1); - grub_printf (" Booting \'%s\'\n\n", e->title); - - run_menu_entry (e); - - /* Deal with a fallback entry. */ - /* FIXME: Multiple fallback entries like GRUB Legacy. */ - fallback_entry = get_entry_number ("fallback"); - if (fallback_entry >= 0) - { - grub_print_error (); - grub_errno = GRUB_ERR_NONE; - - e = get_entry (menu, fallback_entry); - grub_env_unset ("fallback"); - grub_printf ("\n Falling back to \'%s\'\n\n", e->title); - run_menu_entry (e); - } - - if (grub_errno != GRUB_ERR_NONE) - { - grub_print_error (); - grub_errno = GRUB_ERR_NONE; - - grub_wait_after_message (); - } + grub_menu_execute_with_fallback (menu, e, &execution_callback, 0); } + + return GRUB_ERR_NONE; } + +struct grub_menu_viewer grub_normal_terminal_menu_viewer = +{ + .name = "terminal", + .show_menu = show_menu +}; === modified file 'term/gfxterm.c' --- term/gfxterm.c 2008-01-01 12:02:07 +0000 +++ term/gfxterm.c 2008-07-19 19:56:06 +0000 @@ -28,14 +28,12 @@ #include #include #include +#include #include +#include #define DEFAULT_VIDEO_WIDTH 640 #define DEFAULT_VIDEO_HEIGHT 480 -#define DEFAULT_VIDEO_FLAGS 0 - -#define DEFAULT_CHAR_WIDTH 8 -#define DEFAULT_CHAR_HEIGHT 16 #define DEFAULT_BORDER_WIDTH 10 @@ -91,6 +89,9 @@ unsigned int cursor_y; int cursor_state; + /* Font settings. */ + grub_font_t font; + /* Terminal color settings. */ grub_uint8_t standard_color_setting; grub_uint8_t normal_color_setting; @@ -107,10 +108,20 @@ struct grub_colored_char *text_buffer; }; +static int refcount; +static struct grub_video_render_target *render_target; +static grub_video_rect_t window; static struct grub_virtual_screen virtual_screen; +static grub_gfxterm_repaint_callback_t repaint_callback; + +static grub_err_t init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width); + +static void destroy_window (void); + static grub_dl_t my_mod; -static struct grub_video_mode_info mode_info; static struct grub_video_render_target *text_layer; @@ -170,18 +181,25 @@ static grub_err_t grub_virtual_screen_setup (unsigned int x, unsigned int y, - unsigned int width, unsigned int height) + unsigned int width, unsigned int height, + const char *font_name) { /* Free old virtual screen. */ grub_virtual_screen_free (); /* Initialize with default data. */ + virtual_screen.font = grub_font_get (font_name); + if (!virtual_screen.font) + return grub_error (GRUB_ERR_BAD_FONT, + "No font loaded."); virtual_screen.width = width; virtual_screen.height = height; virtual_screen.offset_x = x; virtual_screen.offset_y = y; - virtual_screen.char_width = DEFAULT_CHAR_WIDTH; - virtual_screen.char_height = DEFAULT_CHAR_HEIGHT; + virtual_screen.char_width = + grub_font_get_max_char_width (virtual_screen.font); + virtual_screen.char_height = + grub_font_get_max_char_height (virtual_screen.font); virtual_screen.cursor_x = 0; virtual_screen.cursor_y = 0; virtual_screen.cursor_state = 1; @@ -227,281 +245,129 @@ } static grub_err_t +init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width) +{ + /* Clean up any prior instance. */ + destroy_window (); + + /* Create virtual screen. */ + if (grub_virtual_screen_setup (border_width, border_width, + width - 2 * border_width, + height - 2 * border_width, + font_name) + != GRUB_ERR_NONE) + { + return grub_errno; + } + + /* Set the render target. */ + render_target = target; + + /* Set window bounds. */ + window.x = x; + window.y = y; + window.width = width; + window.height = height; + + /* Mark whole window as dirty. */ + dirty_region_reset (); + dirty_region_add (0, 0, width, height); + + return (grub_errno = GRUB_ERR_NONE); +} + +grub_err_t +grub_gfxterm_init_window (struct grub_video_render_target *target, + int x, int y, int width, int height, + const char *font_name, int border_width) +{ + grub_errno = GRUB_ERR_NONE; + if (refcount++ == 0) + init_window (target, x, y, width, height, font_name, border_width); + return grub_errno; +} + +static grub_err_t grub_gfxterm_init (void) { - char *modevar; - int width = DEFAULT_VIDEO_WIDTH; - int height = DEFAULT_VIDEO_HEIGHT; - int depth = -1; - int flags = DEFAULT_VIDEO_FLAGS; - grub_video_color_t color; + /* If gfxterm has already been initialized by calling the init_window + function, then leave it alone when it is set as the current terminal. */ + if (refcount++ != 0) + return GRUB_ERR_NONE; /* Parse gfxmode environment variable if set. */ - modevar = grub_env_get ("gfxmode"); - if (modevar) - { - char *tmp; - char *next_mode; - char *current_mode; - char *param; - char *value; - int mode_found = 0; - - /* Take copy of env.var. as we don't want to modify that. */ - tmp = grub_strdup (modevar); - modevar = tmp; - - if (grub_errno != GRUB_ERR_NONE) - return grub_errno; - - /* Initialize next mode. */ - next_mode = modevar; - - /* Loop until all modes has been tested out. */ - while (next_mode != NULL) - { - /* Use last next_mode as current mode. */ - tmp = next_mode; - - /* Reset video mode settings. */ - width = DEFAULT_VIDEO_WIDTH; - height = DEFAULT_VIDEO_HEIGHT; - depth = -1; - flags = DEFAULT_VIDEO_FLAGS; - - /* Save position of next mode and separate modes. */ - next_mode = grub_strchr(next_mode, ';'); - if (next_mode) - { - *next_mode = 0; - next_mode++; - } - - /* Skip whitespace. */ - while (grub_isspace (*tmp)) - tmp++; - - /* Initialize token holders. */ - current_mode = tmp; - param = tmp; - value = NULL; - - /* Parse x[x]*/ - - /* Find width value. */ - value = param; - param = grub_strchr(param, 'x'); - if (param == NULL) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - *param = 0; - param++; - - width = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - /* Find height value. */ - value = param; - param = grub_strchr(param, 'x'); - if (param == NULL) - { - height = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - } - else - { - /* We have optional color depth value. */ - *param = 0; - param++; - - height = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - - /* Convert color depth value. */ - value = param; - depth = grub_strtoul (value, 0, 0); - if (grub_errno != GRUB_ERR_NONE) - { - grub_err_t rc; - - /* First setup error message. */ - rc = grub_error (GRUB_ERR_BAD_ARGUMENT, - "Invalid mode: %s\n", - current_mode); - - /* Free memory before returning. */ - grub_free (modevar); - - return rc; - } - } - - /* Try out video mode. */ - - /* If we have 8 or less bits, then assume that it is indexed color mode. */ - if ((depth <= 8) && (depth != -1)) - flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; - - /* We have more than 8 bits, then assume that it is RGB color mode. */ - if (depth > 8) - flags |= GRUB_VIDEO_MODE_TYPE_RGB; - - /* If user requested specific depth, forward that information to driver. */ - if (depth != -1) - flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) - & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; - - /* Try to initialize requested mode. Ignore any errors. */ - grub_error_push (); - if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) - { - grub_error_pop (); - continue; - } - - /* Figure out what mode we ended up. */ - if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) - { - /* Couldn't get video mode info, restore old mode and continue to next one. */ - grub_error_pop (); - - grub_video_restore (); - continue; - } - - /* Restore state of error stack. */ - grub_error_pop (); - - /* Mode found! Exit loop. */ - mode_found = 1; - break; - } - - /* Free memory. */ - grub_free (modevar); - - if (!mode_found) - return grub_error (GRUB_ERR_BAD_ARGUMENT, - "No suitable mode found."); - } - else - { - /* No gfxmode variable set, use defaults. */ - - /* If we have 8 or less bits, then assume that it is indexed color mode. */ - if ((depth <= 8) && (depth != -1)) - flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; - - /* We have more than 8 bits, then assume that it is RGB color mode. */ - if (depth > 8) - flags |= GRUB_VIDEO_MODE_TYPE_RGB; - - /* If user requested specific depth, forward that information to driver. */ - if (depth != -1) - flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) - & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; - - /* Initialize user requested mode. */ - if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) - return grub_errno; - - /* Figure out what mode we ended up. */ - if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) - { - grub_video_restore (); - return grub_errno; - } + const char *modevar = grub_env_get ("gfxmode"); + if (grub_video_setup_preferred_mode (modevar, 0, + DEFAULT_VIDEO_WIDTH, + DEFAULT_VIDEO_HEIGHT) + != GRUB_ERR_NONE) + return grub_errno; + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + { + grub_video_restore (); + return grub_errno; } /* Make sure screen is black. */ - color = grub_video_map_rgb (0, 0, 0); - grub_video_fill_rect (color, 0, 0, mode_info.width, mode_info.height); + grub_video_fill_rect (grub_video_map_rgb (0, 0, 0), + 0, 0, mode_info.width, mode_info.height); bitmap = 0; + /* Select the font to use. */ + char *font_name = grub_env_get ("gfxterm_font"); + if (!font_name) + font_name = ""; /* Allow fallback to any font. */ + /* Leave borders for virtual screen. */ - width = mode_info.width - (2 * DEFAULT_BORDER_WIDTH); - height = mode_info.height - (2 * DEFAULT_BORDER_WIDTH); - - /* Create virtual screen. */ - if (grub_virtual_screen_setup (DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_WIDTH, - width, height) != GRUB_ERR_NONE) + if (init_window (GRUB_VIDEO_RENDER_TARGET_DISPLAY, + 0, 0, mode_info.width, mode_info.height, + font_name, + DEFAULT_BORDER_WIDTH) != GRUB_ERR_NONE) { grub_video_restore (); return grub_errno; } - /* Mark whole screen as dirty. */ - dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); - return (grub_errno = GRUB_ERR_NONE); } +static void +destroy_window (void) +{ + if (bitmap) + { + grub_video_bitmap_destroy (bitmap); + bitmap = 0; + } + + repaint_callback = 0; + grub_virtual_screen_free (); +} + +void +grub_gfxterm_destroy_window (void) +{ + if (--refcount == 0) + destroy_window (); +} + static grub_err_t grub_gfxterm_fini (void) { - if (bitmap) + /* Don't destroy an explicitly initialized terminal instance when it is + unset as the current terminal. */ + if (--refcount == 0) { - grub_video_bitmap_destroy (bitmap); - bitmap = 0; + destroy_window (); + grub_video_restore (); } - grub_virtual_screen_free (); - - grub_video_restore (); - - return GRUB_ERR_NONE; + return (grub_errno = GRUB_ERR_NONE); } static void @@ -509,9 +375,15 @@ unsigned int width, unsigned int height) { grub_video_color_t color; - - grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); - + grub_video_rect_t saved_view; + + grub_video_set_active_render_target (render_target); + /* Save viewport and set it to our window. */ + grub_video_get_viewport ((unsigned *) &saved_view.x, + (unsigned *) &saved_view.y, + (unsigned *) &saved_view.width, + (unsigned *) &saved_view.height); + grub_video_set_viewport (window.x, window.y, window.width, window.height); if (bitmap) { @@ -578,6 +450,14 @@ y - virtual_screen.offset_y, width, height); } + + /* Restore saved viewport. */ + grub_video_set_viewport (saved_view.x, saved_view.y, + saved_view.width, saved_view.height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + + if (repaint_callback) + repaint_callback (x, y, width, height); } static void @@ -661,11 +541,12 @@ write_char (void) { struct grub_colored_char *p; - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; grub_video_color_t color; grub_video_color_t bgcolor; unsigned int x; unsigned int y; + int ascent; /* Find out active character. */ p = (virtual_screen.text_buffer @@ -675,7 +556,8 @@ p -= p->index; /* Get glyph for character. */ - grub_font_get_glyph (p->code, &glyph); + glyph = grub_font_get_glyph (virtual_screen.font, p->code); + ascent = grub_font_get_ascent (virtual_screen.font); color = p->fg_color; bgcolor = p->bg_color; @@ -685,13 +567,13 @@ /* Render glyph to text layer. */ grub_video_set_active_render_target (text_layer); - grub_video_fill_rect (bgcolor, x, y, glyph.width, glyph.height); - grub_video_blit_glyph (&glyph, color, x, y); + grub_video_fill_rect (bgcolor, x, y, glyph->width, glyph->height); + grub_video_blit_glyph (glyph, color, x, y + ascent); grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); /* Mark character to be drawn. */ dirty_region_add (virtual_screen.offset_x + x, virtual_screen.offset_y + y, - glyph.width, glyph.height); + glyph->width, glyph->height); } static void @@ -705,7 +587,8 @@ /* Determine cursor properties and position on text layer. */ x = virtual_screen.cursor_x * virtual_screen.char_width; - y = ((virtual_screen.cursor_y + 1) * virtual_screen.char_height) - 3; + y = (virtual_screen.cursor_y * virtual_screen.char_height + + grub_font_get_ascent (virtual_screen.font)); width = virtual_screen.char_width; height = 2; @@ -769,7 +652,16 @@ dirty_region_add_virtualscreen (); } else - { + { + grub_video_rect_t saved_view; + grub_video_set_active_render_target (render_target); + /* Save viewport and set it to our window. */ + grub_video_get_viewport ((unsigned *) &saved_view.x, + (unsigned *) &saved_view.y, + (unsigned *) &saved_view.width, + (unsigned *) &saved_view.height); + grub_video_set_viewport (window.x, window.y, window.width, window.height); + /* Clear new border area. */ grub_video_fill_rect (color, virtual_screen.offset_x, virtual_screen.offset_y, @@ -778,10 +670,18 @@ /* Scroll physical screen. */ grub_video_scroll (color, 0, -virtual_screen.char_height); + /* Restore saved viewport. */ + grub_video_set_viewport (saved_view.x, saved_view.y, + saved_view.width, saved_view.height); + grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); + /* Draw cursor if visible. */ if (virtual_screen.cursor_state) write_cursor (); } + + if (repaint_callback) + repaint_callback (window.x, window.y, window.width, window.height); } static void @@ -822,14 +722,18 @@ } else { - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; struct grub_colored_char *p; + unsigned char_width; /* Get properties of the character. */ - grub_font_get_glyph (c, &glyph); + glyph = grub_font_get_glyph (virtual_screen.font, c); + + /* TODO [CDB] Fix wide characters. Bi-width font? */ + char_width = 1; /* If we are about to exceed line length, wrap to next line. */ - if (virtual_screen.cursor_x + glyph.char_width > virtual_screen.columns) + if (virtual_screen.cursor_x + char_width > virtual_screen.columns) grub_putchar ('\n'); /* Find position on virtual screen, and fill information. */ @@ -839,18 +743,18 @@ p->code = c; p->fg_color = virtual_screen.fg_color; p->bg_color = virtual_screen.bg_color; - p->width = glyph.char_width - 1; + p->width = char_width - 1; p->index = 0; /* If we have large glyph, add fixup info. */ - if (glyph.char_width > 1) + if (char_width > 1) { unsigned i; - for (i = 1; i < glyph.char_width; i++) + for (i = 1; i < char_width; i++) { p[i].code = ' '; - p[i].width = glyph.char_width - 1; + p[i].width = char_width - 1; p[i].index = i; } } @@ -859,7 +763,7 @@ write_char (); /* Make sure we scroll screen when needed and wrap line correctly. */ - virtual_screen.cursor_x += glyph.char_width; + virtual_screen.cursor_x += char_width; if (virtual_screen.cursor_x >= virtual_screen.columns) { virtual_screen.cursor_x = 0; @@ -877,13 +781,17 @@ } static grub_ssize_t -grub_gfxterm_getcharwidth (grub_uint32_t c) +grub_gfxterm_getcharwidth (grub_uint32_t c __attribute__((unused))) { - struct grub_font_glyph glyph; - - grub_font_get_glyph (c, &glyph); - - return glyph.char_width; +#if 0 + struct grub_font_glyph *glyph; + + glyph = grub_font_get_glyph (c); + + return glyph->char_width; +#else + return 1; /* TODO [CDB] Fix wide characters. */ +#endif } static grub_uint16_t @@ -945,7 +853,8 @@ /* Clear text layer. */ grub_video_set_active_render_target (text_layer); color = virtual_screen.bg_color; - grub_video_fill_rect (color, 0, 0, mode_info.width, mode_info.height); + grub_video_fill_rect (color, 0, 0, + virtual_screen.width, virtual_screen.height); grub_video_set_active_render_target (GRUB_VIDEO_RENDER_TARGET_DISPLAY); /* Mark virtual screen to be redrawn. */ @@ -1014,8 +923,23 @@ dirty_region_redraw (); } +void +grub_gfxterm_set_repaint_callback (grub_gfxterm_repaint_callback_t func) +{ + repaint_callback = func; +} + +/* Option array indices. */ +#define BACKGROUND_CMD_ARGINDEX_MODE 0 + +static const struct grub_arg_option background_image_cmd_options[] = { + {"mode", 'm', 0, "Background image mode (`stretch', `normal').", 0, + ARG_TYPE_STRING}, + {0, 0, 0, 0, 0, 0} +}; + static grub_err_t -grub_gfxterm_background_image_cmd (struct grub_arg_list *state __attribute__ ((unused)), +grub_gfxterm_background_image_cmd (struct grub_arg_list *state, int argc, char **args) { @@ -1031,7 +955,7 @@ /* Mark whole screen as dirty. */ dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); + dirty_region_add (0, 0, window.width, window.height); } /* If filename was provided, try to load that. */ @@ -1042,16 +966,40 @@ if (grub_errno != GRUB_ERR_NONE) return grub_errno; + /* Determine if the bitmap should be scaled to fit the screen. */ + if (!state[BACKGROUND_CMD_ARGINDEX_MODE].set + || grub_strcmp (state[BACKGROUND_CMD_ARGINDEX_MODE].arg, + "stretch") == 0) + { + if (window.width != (int) grub_video_bitmap_get_width (bitmap) + || window.height != (int) grub_video_bitmap_get_height (bitmap)) + { + struct grub_video_bitmap *scaled_bitmap; + grub_video_bitmap_create_scaled (&scaled_bitmap, + window.width, + window.height, + bitmap, + GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST); + if (grub_errno == GRUB_ERR_NONE) + { + /* Replace the original bitmap with the scaled one. */ + grub_video_bitmap_destroy (bitmap); + bitmap = scaled_bitmap; + } + } + } + + /* If bitmap was loaded correctly, display it. */ if (bitmap) { /* Determine bitmap dimensions. */ bitmap_width = grub_video_bitmap_get_width (bitmap); - bitmap_height = grub_video_bitmap_get_width (bitmap); + bitmap_height = grub_video_bitmap_get_height (bitmap); /* Mark whole screen as dirty. */ dirty_region_reset (); - dirty_region_add (0, 0, mode_info.width, mode_info.height); + dirty_region_add (0, 0, window.width, window.height); } } @@ -1082,9 +1030,16 @@ .next = 0 }; +grub_term_t +grub_gfxterm_get_term (void) +{ + return &grub_video_term; +} + GRUB_MOD_INIT(term_gfxterm) { my_mod = mod; + refcount = 0; grub_term_register (&grub_video_term); grub_register_command ("background_image", @@ -1092,7 +1047,7 @@ GRUB_COMMAND_FLAG_BOTH, "background_image", "Load background image for active terminal", - 0); + background_image_cmd_options); } GRUB_MOD_FINI(term_gfxterm) === modified file 'term/i386/pc/vesafb.c' --- term/i386/pc/vesafb.c 2007-12-30 08:52:06 +0000 +++ term/i386/pc/vesafb.c 2008-07-03 14:12:08 +0000 @@ -250,10 +250,11 @@ break; default: - return grub_font_get_glyph (code, bitmap, width); + return grub_font_get_glyph_any (code, bitmap, width); } } + /* TODO [CDB] This is wrong for the new font module. Should it be fixed? */ if (bitmap) grub_memcpy (bitmap, vga_font + code * virtual_screen.char_height, === modified file 'term/i386/pc/vga.c' --- term/i386/pc/vga.c 2008-01-21 15:48:27 +0000 +++ term/i386/pc/vga.c 2008-07-03 14:12:08 +0000 @@ -65,6 +65,7 @@ static struct colored_char text_buf[TEXT_WIDTH * TEXT_HEIGHT]; static unsigned char saved_map_mask; static int page = 0; +static grub_font_t font = 0; #define SEQUENCER_ADDR_PORT 0x3C4 #define SEQUENCER_DATA_PORT 0x3C5 @@ -161,6 +162,9 @@ saved_map_mask = get_map_mask (); set_map_mask (0x0f); set_start_address (PAGE_OFFSET (page)); + font = grub_font_get (""); /* Choose any font, for now. */ + if (!font) + return grub_error (GRUB_ERR_BAD_FONT, "No font loaded."); return GRUB_ERR_NONE; } @@ -185,7 +189,7 @@ write_char (void) { struct colored_char *p = text_buf + xpos + ypos * TEXT_WIDTH; - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; unsigned char *mem_base; unsigned plane; @@ -194,7 +198,7 @@ p -= p->index; /* Get glyph for character. */ - grub_font_get_glyph (p->code, &glyph); + glyph = grub_font_get_glyph (font, p->code); for (plane = 0x01; plane <= 0x08; plane <<= 1) { @@ -210,17 +214,21 @@ { unsigned i; - for (i = 0; i < glyph.char_width && offset < 32; i++) + unsigned char_width = 1; /* TODO [CDB] Figure out wide characters. */ + /* TODO [CDB] Re-implement glyph drawing for vga module. */ +#if 0 + for (i = 0; i < char_width && offset < 32; i++) { unsigned char fg_mask, bg_mask; - fg_mask = (p->fg_color & plane) ? glyph.bitmap[offset] : 0; - bg_mask = (p->bg_color & plane) ? ~(glyph.bitmap[offset]) : 0; + fg_mask = (p->fg_color & plane) ? glyph->bitmap[offset] : 0; + bg_mask = (p->bg_color & plane) ? ~(glyph->bitmap[offset]) : 0; offset++; if (check_vga_mem (mem + i)) mem[i] = (fg_mask | bg_mask); } +#endif /* 0 */ } } @@ -320,36 +328,37 @@ } else { - struct grub_font_glyph glyph; + struct grub_font_glyph *glyph; struct colored_char *p; + unsigned char_width = 1; - grub_font_get_glyph(c, &glyph); + glyph = grub_font_get_glyph(font, c); - if (xpos + glyph.char_width > TEXT_WIDTH) + if (xpos + char_width > TEXT_WIDTH) grub_putchar ('\n'); p = text_buf + xpos + ypos * TEXT_WIDTH; p->code = c; p->fg_color = fg_color; p->bg_color = bg_color; - p->width = glyph.char_width - 1; + p->width = char_width - 1; p->index = 0; - if (glyph.char_width > 1) + if (char_width > 1) { unsigned i; - for (i = 1; i < glyph.char_width; i++) + for (i = 1; i < char_width; i++) { p[i].code = ' '; - p[i].width = glyph.char_width - 1; + p[i].width = char_width - 1; p[i].index = i; } } write_char (); - xpos += glyph.char_width; + xpos += char_width; if (xpos >= TEXT_WIDTH) { xpos = 0; @@ -381,11 +390,16 @@ static grub_ssize_t grub_vga_getcharwidth (grub_uint32_t c) { +#if 0 struct grub_font_glyph glyph; - grub_font_get_glyph (c, &glyph); + glyph = grub_font_get_glyph (c); return glyph.char_width; +#else + /* TODO [CDB] Glyph wide characters? vga? */ + return 1; +#endif } static grub_uint16_t === added directory 'util/fonttool' === added file 'util/fonttool/build.xml' --- util/fonttool/build.xml 1970-01-01 00:00:00 +0000 +++ util/fonttool/build.xml 2008-07-03 14:08:39 +0000 @@ -0,0 +1,23 @@ + + GRUB 2 font tool. + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/fonttool.iml' --- util/fonttool/fonttool.iml 1970-01-01 00:00:00 +0000 +++ util/fonttool/fonttool.iml 2008-07-03 14:08:39 +0000 @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/fonttool.ipr' --- util/fonttool/fonttool.ipr 1970-01-01 00:00:00 +0000 +++ util/fonttool/fonttool.ipr 2008-07-03 14:08:39 +0000 @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + === added file 'util/fonttool/makepf2.sh' --- util/fonttool/makepf2.sh 1970-01-01 00:00:00 +0000 +++ util/fonttool/makepf2.sh 2008-08-07 23:50:20 +0000 @@ -0,0 +1,17 @@ +#!/bin/sh + +# This script will generate GRUB 2 PFF2 font files out of BDF. +# You need the GRUB font utilities "Convert" program for this. + +DIR=$( dirname $0 ) + +for font in "$@" +do + out=`basename "${font}" | cut -d'.' -f1` + out="${out}.pf2" + echo "Converting ${font} -> ${out} ..." + java -cp "${DIR}/build/fonttool.jar" \ + org.gnu.grub.fonttool.Converter \ + --in="${font}" \ + --out="${out}" +done === added directory 'util/fonttool/src' === added directory 'util/fonttool/src/org' === added directory 'util/fonttool/src/org/gnu' === added directory 'util/fonttool/src/org/gnu/grub' === added directory 'util/fonttool/src/org/gnu/grub/fonttool' === added file 'util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java' --- util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/BDFFont.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,24 @@ +package org.gnu.grub.fonttool; + +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; + +public class BDFFont { + private Map glyphs = new HashMap(); + private int height = 0; + + void putGlyph(char ch, BufferedImage glyph) { + glyphs.put(ch, glyph); + if (glyph.getHeight() > height) + height = glyph.getHeight(); + } + + public BufferedImage getGlyph(char c) { + return glyphs.get(c); + } + + public int getHeight() { + return height; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java' --- util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/BDFLoader.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,254 @@ +package org.gnu.grub.fonttool; + +import java.io.*; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BDFLoader { + private final BufferedReader in; + private final Font font; + private int maxCharWidth; + private int maxCharHeight; + + BDFLoader(BufferedReader in) { + this.in = in; + this.font = new Font(); + this.maxCharWidth = 0; + this.maxCharHeight = 0; + } + + public static boolean isBDFFile(String filename) { + DataInputStream in = null; + try { + in = new DataInputStream(new FileInputStream(filename)); + final String signature = "STARTFONT "; + byte[] b = new byte[signature.length()]; + in.readFully(b); + in.close(); + + String s = new String(b, "US-ASCII"); + return signature.equals(s); + } catch (IOException e) { + if (in != null) { + try { + in.close(); + } catch (IOException e1) { + // Ignore. + } + } + return false; + } + } + + public static Font loadFontResource(String resourceName) throws IOException { + InputStream in = BDFLoader.class.getClassLoader().getResourceAsStream(resourceName); + if (in == null) + throw new FileNotFoundException("Font resource " + resourceName + " not found"); + return loadFontFromStream(in); + } + + public static Font loadFontFile(String filename) throws IOException { + InputStream in = new FileInputStream(filename); + return loadFontFromStream(in); + } + + private static Font loadFontFromStream(InputStream inStream) throws IOException { + BufferedReader in; + try { + in = new BufferedReader(new InputStreamReader(inStream, "ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Encoding not supported: " + e.getMessage(), e); + } + return new BDFLoader(in).loadFont(); + } + + private Font loadFont() throws IOException { + loadFontInfo(); + while (loadChar()) { + /* Loop. */ + } + + font.setMaxCharWidth(maxCharWidth); + font.setMaxCharHeight(maxCharHeight); + return font; + } + + private void loadFontInfo() throws IOException { + final Pattern stringSettingPattern = Pattern.compile("^(\\w+)\\s+\"([^\"]+)\"$"); + String line; + // Load the global font information that appears before CHARS. + final int UNSET = Integer.MIN_VALUE; + font.setAscent(UNSET); + font.setDescent(UNSET); + font.setFamily("Unknown"); + font.setBold(false); + font.setItalic(false); + font.setPointSize(Font.UNKNOWN_POINT_SIZE); + do { + line = in.readLine(); + if (line == null) + throw new IOException("BDF format error: end of file while " + + "reading global font information"); + + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens()) { + String name = st.nextToken(); + if (name.equals("FONT_ASCENT")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + font.setAscent(Integer.parseInt(st.nextToken())); + } else if (name.equals("FONT_DESCENT")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + font.setDescent(Integer.parseInt(st.nextToken())); + } else if (name.equals("POINT_SIZE")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: " + + "no tokens after " + name); + // Divide by 10, since it is stored X10. + font.setPointSize(Integer.parseInt(st.nextToken()) / 10); + } else if (name.equals("FAMILY_NAME")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + font.setFamily(matcher.group(2)); + } else if (name.equals("WEIGHT_NAME")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + String weightName = matcher.group(2); + font.setBold("bold".equalsIgnoreCase(weightName)); + } else if (name.equals("SLANT")) { + Matcher matcher = stringSettingPattern.matcher(line); + if (!matcher.matches()) + throw new IOException("BDF format error: " + + "line doesn't match string " + + "setting pattern: " + line); + String slantType = matcher.group(2); + font.setItalic(!"R".equalsIgnoreCase(slantType)); + } else if (name.equals("CHARS")) { + // This is the end of the global font information and + // the beginning of the character definitions. + break; + } else { + // Skip other fields. + } + } + } while (true); + + if (font.getAscent() == UNSET) + throw new IOException("BDF format error: no FONT_ASCENT property"); + if (font.getDescent() == UNSET) + throw new IOException("BDF format error: no FONT_DESCENT property"); + } + + private boolean loadChar() throws IOException { + String line; + // Find start of character + do { + line = in.readLine(); + if (line == null) + return false; + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens() && st.nextToken().equals("STARTCHAR")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no character name after STARTCHAR"); + break; + } + } while (true); + + // Find properties + final int UNSET = Integer.MIN_VALUE; + int codePoint = UNSET; + int bbx = UNSET; + int bby = UNSET; + int bbox = UNSET; + int bboy = UNSET; + int dwidth = UNSET; + do { + line = in.readLine(); + if (line == null) + return false; + StringTokenizer st = new StringTokenizer(line); + if (st.hasMoreTokens()) { + String field = st.nextToken(); + if (field.equals("ENCODING")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no encoding # after ENCODING"); + String codePointStr = st.nextToken(); + codePoint = Integer.parseInt(codePointStr); + } else if (field.equals("BBX")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no tokens after BBX"); + bbx = Integer.parseInt(st.nextToken()); + bby = Integer.parseInt(st.nextToken()); + bbox = Integer.parseInt(st.nextToken()); + bboy = Integer.parseInt(st.nextToken()); + } else if (field.equals("DWIDTH")) { + if (!st.hasMoreTokens()) + throw new IOException("BDF format error: no tokens after DWIDTH"); + dwidth = Integer.parseInt(st.nextToken()); + int dwidthY = Integer.parseInt(st.nextToken()); + // The DWIDTH Y value should be zero for any normal font. + if (dwidthY != 0) { + throw new IOException("BDF format error: dwidth Y value" + + "is nonzero (" + dwidthY + ") " + + "for char " + codePoint + "."); + } + } else if (field.equals("BITMAP")) { + break; // now read the bitmap + } + } + } while (true); + + if (codePoint == UNSET) + throw new IOException("BDF format error: " + + "no code point set"); + if (bbx == UNSET || bby == UNSET) + throw new IOException("BDF format error: " + + "bbx/bby missing: " + bbx + ", " + bby + + " for char " + codePoint); + + if (bbox == UNSET || bboy == UNSET) + throw new IOException("BDF format error: " + + "bbox/bboy missing: " + bbox + ", " + bboy + + " for char " + codePoint); + + if (dwidth == UNSET) + throw new IOException("BDF format error: " + + "dwidth missing for char " + codePoint); + + final int glyphWidth = bbx; + final int glyphHeight = bby; + if (glyphWidth > maxCharWidth) + maxCharWidth = glyphWidth; + if (glyphHeight > maxCharHeight) + maxCharHeight = glyphHeight; + + // Read the bitmap + Glyph glyph = new Glyph(codePoint, glyphWidth, glyphHeight, bbox, bboy, dwidth); + for (int y = 0; y < glyphHeight; y++) { + line = in.readLine(); + if (line == null) + return false; + for (int b = 0; b < line.length(); b++) { + int v = Integer.parseInt(Character.toString(line.charAt(b)), 16); + for (int x = b * 4, i = 0; i < 4 && x < glyphWidth; x++, i++) { + boolean set = (v & 0x8) != 0; + v <<= 1; + glyph.setPixel(x, y, set); + } + } + } + + font.putGlyph(codePoint, glyph); + return true; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharDefs.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,135 @@ +package org.gnu.grub.fonttool; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.TreeMap; + +class CharDefs { + private boolean debug = "1".equals(System.getProperty("fonttool.debug")); + private TreeMap glyphs; + private ByteArrayOutputStream charDefsData; + private int maxCharWidth; + private int maxCharHeight; + private HashMap charIndex; + + public CharDefs(Font font) { + this.glyphs = font.getGlyphs(); + this.charIndex = null; + this.charDefsData = null; + + calculateMaxSizes(); + } + + private void calculateMaxSizes() { + maxCharWidth = 0; + maxCharHeight = 0; + for (Glyph glyph : glyphs.values()) { + final int w = glyph.getWidth(); + final int h = glyph.getHeight(); + if (w > maxCharWidth) + maxCharWidth = w; + if (h > maxCharHeight) + maxCharHeight = h; + } + } + + void buildDefinitions() { + charIndex = new HashMap(); + HashMap charDefIndex = new HashMap(); + charDefsData = new ByteArrayOutputStream(); + DataOutputStream charDefs = new DataOutputStream(charDefsData); + try { + // Loop through all the glyphs, writing the glyph data to the + // in-memory byte stream, collapsing duplicate glyphs, and + // constructing index information. + for (Glyph glyph : glyphs.values()) { + CharDef charDef = new CharDef(glyph.getWidth(), + glyph.getHeight(), + glyph.getBitmap()); + + if (charDefIndex.containsKey(charDef)) { + // Use already-written glyph. + if (debug) + System.out.printf("Duplicate glyph for character U+%04X%n", + glyph.getCodePoint()); + final int charOffset = charDefIndex.get(charDef).intValue(); + final CharStorageInfo info = + new CharStorageInfo(glyph.getCodePoint(), charOffset); + charIndex.put(glyph.getCodePoint(), info); + } else { + // Write glyph data. + final int charOffset = charDefs.size(); + final CharStorageInfo info = + new CharStorageInfo(glyph.getCodePoint(), charOffset); + charIndex.put(glyph.getCodePoint(), info); + + charDefIndex.put(charDef, (long) charOffset); + + charDefs.writeShort(glyph.getWidth()); + charDefs.writeShort(glyph.getHeight()); + charDefs.writeShort(glyph.getBbox()); + charDefs.writeShort(glyph.getBboy()); + charDefs.writeShort(glyph.getDeviceWidth()); + charDefs.write(glyph.getBitmap()); + } + } + } catch (IOException e) { + throw new RuntimeException("Error writing to in-memory byte stream", e); + } + } + + public int getMaxCharWidth() { + return maxCharWidth; + } + + public int getMaxCharHeight() { + return maxCharHeight; + } + + public HashMap getCharIndex() { + if (charIndex == null) throw new IllegalStateException(); + return charIndex; + } + + public byte[] getDefinitionData() { + if (charDefsData == null) throw new IllegalStateException(); + return charDefsData.toByteArray(); + } + + + private static class CharDef { + private final int width; + private final int height; + private final byte[] data; + + public CharDef(int width, int height, byte[] data) { + this.width = width; + this.height = height; + this.data = data; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CharDef charDef = (CharDef) o; + + if (height != charDef.height) return false; + if (width != charDef.width) return false; + if (!Arrays.equals(data, charDef.data)) return false; + + return true; + } + + public int hashCode() { + int result; + result = width; + result = 31 * result + height; + result = 31 * result + Arrays.hashCode(data); + return result; + } + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharMatrix.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,139 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.HashMap; +import java.util.Map; +import javax.swing.*; + +public class CharMatrix extends JComponent { + private Font charFont; + private int zoom = 1; + private int startingChar; + private Map glyphLocations = new HashMap(); + private GlyphSelectionListener glyphSelectionListener; + private Point mouseLocation; + + public interface GlyphSelectionListener { + void glyphSelected(int codePoint); + } + + public CharMatrix() { + addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + int codePoint = getCodePointForGlyphAt(e.getPoint()); + if (codePoint != -1) { + if (glyphSelectionListener != null) + glyphSelectionListener.glyphSelected(codePoint); + } + } + + public void mouseExited(MouseEvent e) { + mouseLocation = null; + repaint(); + } + }); + + addMouseMotionListener(new MouseMotionAdapter() { + public void mouseMoved(MouseEvent e) { + mouseLocation = e.getPoint(); + repaint(); + } + }); + } + + public void setGlyphSelectionListener(GlyphSelectionListener listener) { + this.glyphSelectionListener = listener; + } + + public Font getCharFont() { + return charFont; + } + + public void setCharFont(Font font) { + this.charFont = font; + repaint(); + } + + public int getZoom() { + return zoom; + } + + public void setZoom(int zoom) { + this.zoom = zoom; + repaint(); + } + + public int getStartingChar() { + return startingChar; + } + + public void setStartingChar(int startingChar) { + this.startingChar = startingChar; + repaint(); + } + + public int getCodePointForGlyphAt(Point p) { + for (Rectangle rectangle : glyphLocations.keySet()) { + if (rectangle.contains(p)) + return glyphLocations.get(rectangle); + } + return -1; + } + + protected void paintComponent(Graphics g) { + glyphLocations.clear(); + + if (charFont == null) + return; + + int w = getWidth(); + int h = getHeight(); + int maxCharWidth = charFont.getMaxCharWidth(); + int maxCharHeight = charFont.getMaxCharHeight(); + int space = 2; + int charsPerRow = w / (maxCharWidth + space); + int rows = h / (maxCharHeight + space); + + Rectangle highlightBounds = null; + int codePoint = startingChar; + int y = 3 + charFont.getAscent(); // Point y to the baseline. + for (int row = 0; row < rows; row++) { + int x = 3; + for (int col = 0; col < charsPerRow; col++) { + Glyph glyph = charFont.getGlyph(codePoint); + + if (glyph != null) { + Rectangle charBounds = + CharView.drawGlyph(g, charFont, glyph, x, y, zoom, getForeground()); + // Expand the bounding box for a bigger clickable area. + final int boundsPad = 1; + charBounds.x -= boundsPad; + charBounds.y -= boundsPad; + charBounds.width += 2 * boundsPad; + charBounds.height += 2 * boundsPad; + glyphLocations.put(charBounds, codePoint); + if (mouseLocation != null && charBounds.contains(mouseLocation)) { + highlightBounds = charBounds; + } + } + x += maxCharWidth + space; + codePoint++; + } + + y += maxCharHeight + space; + } + + if (highlightBounds != null) { + g.setColor(Color.blue); + Graphics2D g2 = (Graphics2D) g; + g2.setStroke(new BasicStroke(3f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 2f)); + g2.draw(new Rectangle(highlightBounds.x - 2, highlightBounds.y - 2, + highlightBounds.width + 3, highlightBounds.height + 3)); + g2.setStroke(new BasicStroke(1f)); + } + + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharStorageInfo.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,19 @@ +package org.gnu.grub.fonttool; + +class CharStorageInfo { + private final int codePoint; + private final int fileOffset; + + public CharStorageInfo(int codePoint, int fileOffset) { + this.codePoint = codePoint; + this.fileOffset = fileOffset; + } + + public int getCodePoint() { + return codePoint; + } + + public int getFileOffset() { + return fileOffset; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/CharView.java' --- util/fonttool/src/org/gnu/grub/fonttool/CharView.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/CharView.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,101 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import java.util.Formatter; +import javax.swing.*; + +public class CharView extends JComponent { + private Font charFont = null; + private Glyph glyph = null; + private int zoom = 1; + + public Font getCharFont() { + return charFont; + } + + public void setCharFont(Font charFont) { + this.charFont = charFont; + } + + public Glyph getGlyph() { + return glyph; + } + + public void setGlyph(Glyph glyph) { + this.glyph = glyph; + repaint(); + } + + public void setZoom(int zoom) { + this.zoom = zoom; + } + + public Dimension getPreferredSize() { + if (glyph == null) + return new Dimension(1, 1); + return new Dimension(glyph.getWidth() * zoom, glyph.getHeight() * zoom); + } + + protected void paintComponent(Graphics g) { + if (glyph == null) { + // Draw a gray 'X' indicating no character selected. + g.setColor(Color.gray); + g.drawLine(0, 0, getWidth() - 1, getHeight() - 1); + g.drawLine(getWidth(), 0, 0, getHeight() - 1); + return; + } + + final int x0 = (getWidth() - glyph.getWidth() * zoom) / 2; + final int y0 = getHeight() / 2; + drawGlyph(g, charFont, glyph, x0, y0, zoom, getForeground()); + + g.setFont(new java.awt.Font("SansSerif", java.awt.Font.PLAIN, 12)); + FontMetrics fm = g.getFontMetrics(); + Formatter f = new Formatter(); + f.format("U+%04X", glyph.getCodePoint()); + String str = f.toString(); + g.drawString(str, + (getWidth() - fm.stringWidth(str)) / 2, + getHeight() - fm.getHeight()); + } + + /** + * Draw the specified glyph with its baseline at y0 and the horizontal + * start at x0. + */ + static Rectangle drawGlyph(Graphics g, Font font, Glyph glyph, int startX, int baselineY, int zoom, Color fg) { + // Adjust (x0, y0) so that it puts the bitmap at the right location. + final int offsetX = zoom * glyph.getBbox(); + final int offsetY = zoom * glyph.getBboy(); + final int bitmapLeft = startX + offsetX; + final int bitmapBottom = baselineY - offsetY; + final int bitmapTop = bitmapBottom - zoom * glyph.getHeight(); + + // Draw a gray box around the glyph bounds. + g.setColor(Color.lightGray); + g.drawRect(bitmapLeft - 1, bitmapTop - 1, + glyph.getWidth() * zoom + 1, zoom * glyph.getHeight() + 1); + + // Draw the baseline + g.setColor(new Color(110, 110, 255)); + g.drawLine(startX, baselineY, startX + zoom * glyph.getDeviceWidth(), baselineY); + + // Draw the glyph. + g.setColor(fg); + byte[] bitmap = glyph.getBitmap(); + int bitIndex = 0; + for (int y = 0; y < glyph.getHeight(); y++) { + for (int x = 0; x < glyph.getWidth(); x++, bitIndex++) { + int byteIndex = bitIndex / 8; + int bitShift = bitIndex % 8; + byte b = bitmap[byteIndex]; + boolean bitValue = (b & (0x80 >>> bitShift)) != 0; + if (bitValue) { + g.fillRect(bitmapLeft + x * zoom, bitmapTop + y * zoom, zoom, zoom); + } + } + } + + return new Rectangle(bitmapLeft, bitmapTop, zoom * glyph.getWidth(), zoom * glyph.getHeight()); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Converter.java' --- util/fonttool/src/org/gnu/grub/fonttool/Converter.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Converter.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,78 @@ +package org.gnu.grub.fonttool; + +import java.io.IOException; + +/** + * Program to convert BDF fonts into PFF2 fonts for use with GRUB. + */ +public class Converter { + public static void main(String[] args) { + if (args.length < 1) { + printUsageAndExit(); + } + + String in = null; + String out = null; + + try { + for (String arg : args) { + if (arg.startsWith("--")) { + String option; + String value; + int equalsPos = arg.indexOf('='); + if (equalsPos < 0) { + option = arg.substring(2); + value = null; + } else { + option = arg.substring(2, equalsPos); + value = arg.substring(equalsPos + 1); + } + + if ("in".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + in = value; + } else if ("out".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + out = value; + } + } else { + throw new CommandLineException("Non-option argument `" + arg + "'."); + } + } + if (in == null || out == null) { + throw new CommandLineException("Both --in=X and --out=Y must be specified."); + } + } catch (CommandLineException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + try { + // Read BDF. + Font font = BDFLoader.loadFontFile(in); + + // Write PFF2. + new PFF2Writer(out).writeFont(font); + } catch (IOException e) { + System.err.println("I/O error converting font: " + e); + e.printStackTrace(); + System.exit(1); + } + } + + private static class CommandLineException extends Exception { + public CommandLineException(String message) { + super(message); + } + } + + private static void printUsageAndExit() { + System.err.println("GNU GRUB Font Conversion Tool"); + System.err.println(); + System.err.println("Usage: Converter --in=IN.bdf --out=OUT.pf2"); + System.err.println(); + System.exit(1); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java' --- util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/FileFormatException.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,9 @@ +package org.gnu.grub.fonttool; + +import java.io.IOException; + +public class FileFormatException extends IOException { + public FileFormatException(String msg) { + super(msg); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Font.java' --- util/fonttool/src/org/gnu/grub/fonttool/Font.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Font.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,106 @@ +package org.gnu.grub.fonttool; + +import java.util.TreeMap; + +public class Font { + public static final int UNKNOWN_POINT_SIZE = -1; + + private TreeMap glyphs; + private String family; + private boolean bold; + private boolean italic; + private int pointSize; + private int maxCharWidth; + private int maxCharHeight; + private int ascent; + private int descent; + + public Font() { + glyphs = new TreeMap(); + } + + public String getFamily() { + return family; + } + + public void setFamily(String family) { + this.family = family; + } + + public boolean isBold() { + return bold; + } + + public void setBold(boolean bold) { + this.bold = bold; + } + + public boolean isItalic() { + return italic; + } + + public void setItalic(boolean italic) { + this.italic = italic; + } + + public int getPointSize() { + return pointSize; + } + + public void setPointSize(int pointSize) { + this.pointSize = pointSize; + } + + public int getMaxCharWidth() { + return maxCharWidth; + } + + public void setMaxCharWidth(int maxCharWidth) { + this.maxCharWidth = maxCharWidth; + } + + public int getMaxCharHeight() { + return maxCharHeight; + } + + public void setMaxCharHeight(int maxCharHeight) { + this.maxCharHeight = maxCharHeight; + } + + public int getAscent() { + return ascent; + } + + public void setAscent(int ascent) { + this.ascent = ascent; + } + + public int getDescent() { + return descent; + } + + public void setDescent(int descent) { + this.descent = descent; + } + + public void putGlyph(int codePoint, Glyph glyph) { + glyphs.put(codePoint, glyph); + } + + public TreeMap getGlyphs() { + return glyphs; + } + + public Glyph getGlyph(int codePoint) { + return glyphs.get(codePoint); + } + + public String getStandardName() { + StringBuilder name = new StringBuilder(getFamily()); + if (isBold()) name.append(" Bold"); + if (isItalic()) name.append(" Italic"); + name.append(' '); + name.append(getPointSize()); + return name.toString(); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Glyph.java' --- util/fonttool/src/org/gnu/grub/fonttool/Glyph.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Glyph.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,83 @@ +package org.gnu.grub.fonttool; + +public class Glyph { + private final int codePoint; + private final int width; + private final int height; + + // These define the amounts to shift the character bitmap by + // before drawing it. See + // http://www.adobe.com/devnet/font/pdfs/5005.BDF_Spec.pdf + // and + // http://www.linuxbabble.com/documentation/x/bdf/ + // for explanatory figures. + private final int bbox; + private final int bboy; + + // Number of pixels to advance horizontally from this character's origin + // to the origin of the next character. + private final int deviceWidth; + + // Row-major order, no padding. Rows can break within a byte. + // MSb is first (leftmost/uppermost) pixel. + private final byte[] bitmap; + + public Glyph(int codePoint, int width, int height, int bbox, int bboy, int deviceWidth) { + this(codePoint, width, height, bbox, bboy, deviceWidth, + new byte[(width * height + 7) / 8]); + } + + public Glyph(int codePoint, int width, int height, + int bbox, int bboy, int deviceWidth, + byte[] bitmap) { + this.codePoint = codePoint; + this.width = width; + this.height = height; + this.bboy = bboy; + this.bbox = bbox; + this.deviceWidth = deviceWidth; + this.bitmap = bitmap; + } + + public void setPixel(int x, int y, boolean value) { + if (x < 0 || y < 0 || x >= width || y >= height) + throw new IllegalArgumentException( + "Invalid pixel location (" + x + ", " + y + ") for " + + width + "x" + height + " glyph"); + + int bitIndex = y * width + x; + int byteIndex = bitIndex / 8; + int bitPos = bitIndex % 8; + int v = value ? 0x80 >>> bitPos : 0; + int mask = ~(0x80 >>> bitPos); + bitmap[byteIndex] = (byte) ((bitmap[byteIndex] & mask) | v); + } + + public int getCodePoint() { + return codePoint; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getBbox() { + return bbox; + } + + public int getBboy() { + return bboy; + } + + public int getDeviceWidth() { + return deviceWidth; + } + + public byte[] getBitmap() { + return bitmap; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Loader.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,203 @@ +package org.gnu.grub.fonttool; + +import java.io.EOFException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; + +/** + * Load a PFF2 font completely into memory as an instance of Font. + */ +public class PFF2Loader { + private static final int debug + = Integer.parseInt(System.getProperty("fonttool.debug", "0")); + + private RandomAccessFile f; + private Font font; + + private PFF2Loader(String filename) throws FileNotFoundException { + f = new RandomAccessFile(filename, "r"); + font = new Font(); + } + + public static boolean isPFF2File(String filename) { + RandomAccessFile f = null; + boolean isFont = false; + try { + f = new RandomAccessFile(filename, "r"); + byte[] sectionNameBytes = new byte[4]; + f.readFully(sectionNameBytes); + String sectionName = new String(sectionNameBytes, "US-ASCII"); + if (sectionName.equals(PFF2Sections.FILE)) { + final String expectedType = "PFF2"; + int len = f.readInt(); + if (len == expectedType.length()) { + byte[] typeBytes = new byte[len]; + f.readFully(typeBytes); + String type = new String(typeBytes, "US-ASCII"); + if (type.equals(expectedType)) { + isFont = true; + } + } + } + f.close(); + } catch (IOException e) { + if (f != null) { + try { + f.close(); + } catch (IOException e1) { + // Ignore. + } + } + } + + return isFont; + } + + public static Font loadFontFile(String filename) throws IOException { + PFF2Loader loader = new PFF2Loader(filename); + loader.loadFont(); + return loader.getFont(); + } + + private void loadFont() throws IOException { + String s; + s = openSection(); + if (!PFF2Sections.FILE.equals(s)) + throw new FileFormatException( + "First PFF2 section must be FILE but is `" + s + "'."); + + s = readCompleteSectionAsString(); + if (!"PFF2".equals(s)) + throw new FileFormatException("Unsupported file type `" + s + "'."); + + font.setFamily("Unnamed"); + font.setPointSize(Font.UNKNOWN_POINT_SIZE); + try { + if (debug >= 1) System.out.println("Loading font information..."); + boolean doneLoadingInfo = false; + while (!doneLoadingInfo) { + s = openSection(); + int sectionLength = f.readInt(); + if (debug >= 2) + System.out.println("Section: " + s + " length: " + sectionLength); + if (s.equals(PFF2Sections.MAX_CHAR_WIDTH)) { + int maxCharWidth = readSectionContentsAsUShort(sectionLength); + font.setMaxCharWidth(maxCharWidth); + } else if (s.equals(PFF2Sections.MAX_CHAR_HEIGHT)) { + int maxCharHeight = readSectionContentsAsUShort(sectionLength); + font.setMaxCharHeight(maxCharHeight); + } else if (s.equals(PFF2Sections.FONT_POINT_SIZE)) { + int pointSize = readSectionContentsAsUShort(sectionLength); + font.setPointSize(pointSize); + } else if (s.equals(PFF2Sections.FONT_ASCENT)) { + int ascent = readSectionContentsAsUShort(sectionLength); + font.setAscent(ascent); + } else if (s.equals(PFF2Sections.FONT_DESCENT)) { + int descent = readSectionContentsAsUShort(sectionLength); + font.setDescent(descent); + } else if (s.equals(PFF2Sections.FONT_NAME)) { + String fontName = readSectionContentsAsString(sectionLength); + if (debug >= 1) + System.out.println("Loaded font: " + fontName); + } else if (s.equals(PFF2Sections.FONT_FAMILY)) { + font.setFamily(readSectionContentsAsString(sectionLength)); + } else if (s.equals(PFF2Sections.FONT_WEIGHT)) { + String weightString = readSectionContentsAsString(sectionLength); + font.setBold(weightString.equalsIgnoreCase("bold")); + } else if (s.equals(PFF2Sections.FONT_SLANT)) { + String slantString = readSectionContentsAsString(sectionLength); + font.setItalic(slantString.equalsIgnoreCase("italic")); + } else if (s.equals(PFF2Sections.CHAR_INDEX)) { + loadCharsFromIndex(sectionLength); + } else if (s.equals(PFF2Sections.REMAINDER_IS_DATA)) { + doneLoadingInfo = true; + } else { + // If the section is not handled, then we just skip it. + if (f.skipBytes(sectionLength) != sectionLength) { + System.err.println("Warning: Could not skip " + + sectionLength + + " bytes for section `" + s + "'."); + } + } + } + } catch (EOFException eofException) { + // End of file; done reading font. + } + f.close(); + } + + private void loadCharsFromIndex(int sectionLength) throws IOException { + long indexSectionEnd = f.getFilePointer() + sectionLength; + try { + while (f.getFilePointer() < indexSectionEnd) { + int codePoint = f.readInt(); + int storageFlags = f.readUnsignedByte(); + int offset = f.readInt(); + + if ((storageFlags & 0x07) == 0) { + Glyph g = loadUncompressedGlyph(codePoint, offset); + font.putGlyph(codePoint, g); + } + } + } catch (EOFException eofException) { + // End of file; simple return. + } + } + + private Glyph loadUncompressedGlyph(int codePoint, int fileOffset) throws IOException { + if (debug >= 3) + System.out.printf("Loading uncompressed glyph U+%04X at %d%n", codePoint, fileOffset); + + long savedPos = f.getFilePointer(); + + f.seek(fileOffset); + int width = f.readUnsignedShort(); + int height = f.readUnsignedShort(); + int offsetX = f.readShort(); + int offsetY = f.readShort(); + int deviceWidth = f.readShort(); + if (debug >= 3) System.out.println(" Size=" + width + "x" + height + + " offset=(" + offsetX + ", " + offsetY + + ") dWidth=" + deviceWidth); + byte[] bitmap = new byte[(width * height + 7) / 8]; + f.readFully(bitmap); + + f.seek(savedPos); + return new Glyph(codePoint, width, height, + offsetX, offsetY, deviceWidth, bitmap); + } + + private int readSectionContentsAsUShort(int sectionLength) throws IOException { + if (sectionLength != 2) + throw new FileFormatException( + "For uint16 section, expected 2 bytes but length is " + + sectionLength + "."); + return f.readUnsignedShort(); + } + + private String openSection() throws IOException { + byte[] b = new byte[4]; + f.readFully(b); + return new String(b, Charset.forName("US-ASCII")); + } + + private String readCompleteSectionAsString() throws IOException { + int n = f.readInt(); + return readSectionContentsAsString(n); + } + + private String readSectionContentsAsString(int n) throws IOException { + StringBuilder buf = new StringBuilder(); + while (n-- != 0) { + char c = (char) (f.readByte() & 0xFF); + buf.append(c); + } + return buf.toString(); + } + + public Font getFont() { + return font; + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Sections.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,19 @@ +package org.gnu.grub.fonttool; + +/** + * Section name constants for the PFF2 file format. + */ +public class PFF2Sections { + static final String FILE = "FILE"; + static final String FONT_NAME = "NAME"; + static final String FONT_FAMILY = "FAMI"; + static final String FONT_WEIGHT = "WEIG"; + static final String FONT_SLANT = "SLAN"; + static final String FONT_POINT_SIZE = "PTSZ"; + static final String MAX_CHAR_WIDTH = "MAXW"; + static final String MAX_CHAR_HEIGHT = "MAXH"; + static final String FONT_ASCENT = "ASCE"; + static final String FONT_DESCENT = "DESC"; + static final String CHAR_INDEX = "CHIX"; + static final String REMAINDER_IS_DATA = "DATA"; +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java' --- util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/PFF2Writer.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,133 @@ +package org.gnu.grub.fonttool; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +// TODO Add DEFLATE compressed blocks of characters. +public class PFF2Writer { + private RandomAccessFile f; + private String currentSection; + private long currentSectionStart; + + public PFF2Writer(String filename) throws FileNotFoundException { + this.f = new RandomAccessFile(filename, "rw"); + this.currentSection = null; + this.currentSectionStart = -1; + } + + public void writeFont(Font font) throws IOException { + // Write file type ID header. + writeSection(PFF2Sections.FILE, "PFF2"); + + writeSection(PFF2Sections.FONT_NAME, font.getStandardName()); + writeSection(PFF2Sections.FONT_FAMILY, font.getFamily()); + writeSection(PFF2Sections.FONT_WEIGHT, font.isBold() ? "bold" : "normal"); + writeSection(PFF2Sections.FONT_SLANT, font.isItalic() ? "italic" : "normal"); + if (font.getPointSize() != Font.UNKNOWN_POINT_SIZE) + writeShortSection(PFF2Sections.FONT_POINT_SIZE, font.getPointSize()); + + // Construct character definitions. + CharDefs charDefs = new CharDefs(font); + charDefs.buildDefinitions(); + + // Write max character width and height metrics. + writeShortSection(PFF2Sections.MAX_CHAR_WIDTH, charDefs.getMaxCharWidth()); + writeShortSection(PFF2Sections.MAX_CHAR_HEIGHT, charDefs.getMaxCharHeight()); + writeShortSection(PFF2Sections.FONT_ASCENT, font.getAscent()); + writeShortSection(PFF2Sections.FONT_DESCENT, font.getDescent()); + + // Write character index with pointers to the character definitions. + beginSection(PFF2Sections.CHAR_INDEX); + + // Determine the size of the index, so we can properly refer to the + // character definition offset in the index. The actual number of + // bytes written is compared to the calculated value to ensure we + // are correct. + final int indexStart = (int) f.getFilePointer(); + final int calculatedIndexLength = + font.getGlyphs().size() * (4 + 1 + 4); + final int charDefStart = indexStart + calculatedIndexLength + 8; + + for (CharStorageInfo storageInfo : charDefs.getCharIndex().values()) { + f.writeInt(storageInfo.getCodePoint()); + f.writeByte(0); // Storage flags: bits 1..0 = 00b : uncompressed. + f.writeInt(charDefStart + storageInfo.getFileOffset()); + } + + final int indexEnd = (int) f.getFilePointer(); + if (indexEnd - indexStart != calculatedIndexLength) { + throw new RuntimeException("Incorrect index length calculated, calc=" + + calculatedIndexLength + + " actual=" + (indexEnd - indexStart)); + } + endSection(PFF2Sections.CHAR_INDEX); + + f.writeBytes(PFF2Sections.REMAINDER_IS_DATA); + f.writeInt(-1); // Data takes up the rest of the file. + f.write(charDefs.getDefinitionData()); + + f.close(); + } + + private void beginSection(String sectionName) throws IOException { + verifyOkToBeginSection(sectionName); + + f.writeBytes(sectionName); + f.writeInt(-1); // Placeholder for the section length. + currentSection = sectionName; + currentSectionStart = f.getFilePointer(); + } + + private void endSection(String sectionName) throws IOException { + verifyOkToEndSection(sectionName); + + long sectionEnd = f.getFilePointer(); + long sectionLength = sectionEnd - currentSectionStart; + f.seek(currentSectionStart - 4); + f.writeInt((int) sectionLength); + f.seek(sectionEnd); + currentSection = null; + currentSectionStart = -1; + } + + private void verifyOkToBeginSection(String sectionName) { + if (sectionName.length() != 4) + throw new IllegalArgumentException( + "Section names must be 4 characters: `" + sectionName + "'."); + if (currentSection != null) + throw new IllegalStateException( + "Attempt to start `" + sectionName + + "' section before ending the previous section `" + + currentSection + "'."); + } + + private void verifyOkToEndSection(String sectionName) { + if (sectionName.length() != 4) + throw new IllegalStateException("Invalid section name '" + sectionName + + "'; must be 4 characters."); + if (currentSection == null) + throw new IllegalStateException( + "Attempt to end section `" + sectionName + + "' when no section active."); + if (!sectionName.equals(currentSection)) + throw new IllegalStateException( + "Attempt to end `" + sectionName + + "' section during active section `" + + currentSection + "'."); + } + + private void writeSection(String sectionName, String contents) throws IOException { + verifyOkToBeginSection(sectionName); + f.writeBytes(sectionName); + f.writeInt(contents.length()); + f.writeBytes(contents); + } + + private void writeShortSection(String sectionName, int value) throws IOException { + verifyOkToBeginSection(sectionName); + f.writeBytes(sectionName); + f.writeInt(2); + f.writeShort(value); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/Viewer.java' --- util/fonttool/src/org/gnu/grub/fonttool/Viewer.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/Viewer.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,135 @@ +package org.gnu.grub.fonttool; + +import java.io.File; +import java.io.IOException; +import javax.swing.*; + +/** + * Program to view fonts. + *

+ * Supports PFF2 and BDF format font files. + */ +public class Viewer { + public static void main(String[] args) { + if (args.length < 1) { + printUsageAndExit(); + } + + boolean verbose = false; + String format = null; + String filename = null; + + try { + for (String arg : args) { + if (arg.startsWith("--")) { + String option; + String value; + int equalsPos = arg.indexOf('='); + if (equalsPos < 0) { + option = arg.substring(2); + value = null; + } else { + option = arg.substring(2, equalsPos); + value = arg.substring(equalsPos + 1); + } + + if ("format".equals(option)) { + if (value == null) + throw new CommandLineException(option + " option requires a value."); + format = value; + } else if ("verbose".equals(option)) { + verbose = true; + } + } else { + if (filename == null) { + filename = arg; + } else { + throw new CommandLineException( + "Extra argument after filename: `" + arg + "'."); + } + } + } + if (filename == null) { + throw new CommandLineException("A filename must be specified."); + } + if (!(format == null || "bdf".equals(format) || "pff2".equals(format))) { + throw new CommandLineException("Invalid format: must be `bdf' or `pff2'."); + } + } catch (CommandLineException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + if (!new File(filename).exists()) { + System.err.println("Error: File `" + filename + "' not found."); + System.exit(1); + } + + if (format == null) { + // Autodetect the format. + if (PFF2Loader.isPFF2File(filename)) + format = "pff2"; + else if (BDFLoader.isBDFFile(filename)) + format = "bdf"; + else { + System.err.println("Error: Unable to detect font file format " + + "for `" + filename + "'."); + System.exit(1); + } + if (verbose) + System.out.println("Detected file format `" + format + "'."); + } + + Font font = null; + try { + if ("bdf".equals(format)) { + if (verbose) + System.out.println("Loading BDF font."); + font = BDFLoader.loadFontFile(filename); + } else if ("pff2".equals(format)) { + if (verbose) + System.out.println("Loading PFF2 font."); + font = PFF2Loader.loadFontFile(filename); + } else { + System.err.println("Error: Bad format `" + format + "'"); + System.exit(1); + } + } catch (IOException e) { + System.err.println("I/O error loading font: " + e); + e.printStackTrace(); + System.exit(1); + } + + if (font == null) { + System.err.println("Error: Font not loaded."); + System.exit(1); + } + + if (verbose) { + System.out.println("Loaded font: '" + font.getStandardName() + "'"); + } + + ViewerFrame f = new ViewerFrame(font); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.setLocationRelativeTo(null); // Center the frame on screen. + f.setVisible(true); + } + + private static class CommandLineException extends Exception { + public CommandLineException(String message) { + super(message); + } + } + + private static void printUsageAndExit() { + System.err.println("GNU GRUB Font Viewer"); + System.err.println(); + System.err.println("Usage: Viewer [--format=FORMAT] FILE"); + System.err.println(" --format=bdf The font is in BDF format."); + System.err.println(" --format=pff2 The font is in PFF2 format."); + System.err.println(" If no --format option is given, the type will be"); + System.err.println(" automatically detected."); + System.err.println(); + System.exit(1); + } +} === added file 'util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java' --- util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/src/org/gnu/grub/fonttool/ViewerFrame.java 2008-07-03 14:08:39 +0000 @@ -0,0 +1,61 @@ +package org.gnu.grub.fonttool; + +import java.awt.*; +import javax.swing.*; +import javax.swing.border.BevelBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public class ViewerFrame extends JFrame { + private final Font font; + + private JLabel startingCodePointLabel; + private JSlider startingCodePointSlider; + private CharMatrix charMatrix; + private CharView charView; + private JLabel statusBar; + + public ViewerFrame(Font font) { + this.font = font; + + createComponents(); + buildUI(); + setSize(800, 600); + } + + private void createComponents() { + startingCodePointLabel = new JLabel("Starting Code Point"); + startingCodePointSlider = new JSlider(JSlider.HORIZONTAL, 0, 65535, 0); + startingCodePointSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + charMatrix.setStartingChar(startingCodePointSlider.getValue()); + } + }); + charMatrix = new CharMatrix(); + charMatrix.setCharFont(font); + charMatrix.setGlyphSelectionListener(new CharMatrix.GlyphSelectionListener() { + public void glyphSelected(int codePoint) { + charView.setGlyph(font.getGlyph(codePoint)); + } + }); + charView = new CharView(); + charView.setZoom(3); + charView.setBorder(new BevelBorder(BevelBorder.RAISED)); + statusBar = new JLabel(); + } + + private void buildUI() { + JComponent topPanel = Box.createVerticalBox(); + topPanel.add(startingCodePointLabel); + topPanel.add(startingCodePointSlider); + + JComponent charPanel = new JPanel(new GridLayout(2, 1)); + charPanel.add(charMatrix); + charPanel.add(charView); + + setLayout(new BorderLayout()); + add(topPanel, BorderLayout.NORTH); + add(charPanel, BorderLayout.CENTER); + add(statusBar, BorderLayout.SOUTH); + } +} === added directory 'util/fonttool/test' === added directory 'util/fonttool/test/org' === added directory 'util/fonttool/test/org/gnu' === added directory 'util/fonttool/test/org/gnu/grub' === added directory 'util/fonttool/test/org/gnu/grub/fonttool' === added file 'util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java' --- util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java 1970-01-01 00:00:00 +0000 +++ util/fonttool/test/org/gnu/grub/fonttool/TestGlyph.java 2008-07-03 14:10:25 +0000 @@ -0,0 +1,110 @@ +package org.gnu.grub.fonttool; + +import org.junit.Assert; +import org.junit.Test; + +public class TestGlyph { + @Test + public void testConstructor3x5() { + Glyph g = new Glyph(65, 3, 5, 0, 0, 0); + + // Should be empty upon construction. + assertBitmapEquals("..." + + "..." + + "..." + + "..." + + "...", + g.getBitmap()); + + // Should have ceil(3 * 5 / 8) = ceil(15 / 8) = ceil(1 + 7/8) = 2 bytes. + Assert.assertEquals("Wrong bitmap array size.", + 2, g.getBitmap().length); + } + + @Test + public void testConstructor5x5() { + Glyph g = new Glyph(65, 5, 5, 0, 0, 0); + + // Should be empty upon construction. + assertBitmapEquals("....." + + "....." + + "....." + + "....." + + ".....", + g.getBitmap()); + + // Should have ceil(5 * 5 / 8) = ceil(25 / 8) = ceil(3 + 1/8) = 4 bytes. + Assert.assertEquals("Wrong bitmap array size.", + 4, g.getBitmap().length); + } + + @Test + public void testSetPixel() { + Glyph g = new Glyph(65, 3, 5, 0, 0, 0); + + g.setPixel(0, 0, true); + Assert.assertEquals(0x80, g.getBitmap()[0] & 0xFF); + Assert.assertEquals(0x00, g.getBitmap()[1] & 0xFF); + assertBitmapEquals("X.." + + "..." + + "..." + + "..." + + "...", + g.getBitmap()); + + g.setPixel(2, 1, true); + Assert.assertEquals(0x84, g.getBitmap()[0] & 0xFF); + Assert.assertEquals(0x00, g.getBitmap()[1] & 0xFF); + assertBitmapEquals("X.." + + "..X" + + "..." + + "..." + + "...", + g.getBitmap()); + } + + static void assertBitmapEquals(String expectedBitmap, byte[] actualBitmap) { + Assert.assertNotNull("expected bitmap null", expectedBitmap); + Assert.assertNotNull("actual bitmap null", actualBitmap); + StringBuilder errors = new StringBuilder(); + + int bit = 7; + int byteIndex = -1; + for (int bitIndex = 0; bitIndex < expectedBitmap.length(); bitIndex++) { + char c = expectedBitmap.charAt(bitIndex); + boolean expectedBit; + if (c == '.') { + expectedBit = false; + } else if (c == 'X') { + expectedBit = true; + } else { + throw new IllegalArgumentException("Bad character '" + c + "' in expected bitmap string"); + } + + byteIndex = bitIndex / 8; + byte b = actualBitmap[byteIndex]; + boolean actualBit = (b & (1 << bit)) != 0; + if (actualBit != expectedBit) { + errors.append("[").append(bitIndex) + .append("] wrong (it is ") + .append(actualBit ? "1" : "0").append("). "); + } + + if (bit == 0) { + bit = 7; + } else { + bit--; + } + } + + if (byteIndex != expectedBitmap.length() / 8) { + errors.append("Last byte index wrong, was ") + .append(byteIndex).append(" but expected ") + .append(expectedBitmap.length() - 1); + } + + if (errors.length() != 0) { + Assert.fail("Bitmap wrong: " + errors); + } + } +} === modified file 'util/i386/pc/grub-mkrescue.in' --- util/i386/pc/grub-mkrescue.in 2008-07-12 14:40:50 +0000 +++ util/i386/pc/grub-mkrescue.in 2008-07-19 21:39:24 +0000 @@ -71,7 +71,7 @@ --modules=*) modules=`echo "$option" | sed 's/--modules=//'` ;; --overlay=*) - overlay=`echo "$option" | sed 's/--overlay=//'` ;; + overlay=${overlay}${overlay:+ }`echo "$option" | sed 's/--overlay=//'` ;; --pkglibdir=*) input_dir=`echo "$option" | sed 's/--pkglibdir=//'` ;; --grub-mkimage=*) @@ -124,9 +124,10 @@ echo "insmod $i" done > ${aux_dir}/boot/grub/grub.cfg -if test "x$overlay" = x ; then : ; else - cp -dpR ${overlay}/* ${aux_dir}/ -fi +for d in ${overlay}; do + echo "Overlaying $d" + cp -vdpR ${d}/* ${aux_dir}/ +done if [ "x${image_type}" = xfloppy -o "x${emulation}" = xfloppy ] ; then # build memdisk === modified file 'video/bitmap.c' --- video/bitmap.c 2007-07-21 22:32:33 +0000 +++ video/bitmap.c 2008-07-03 13:49:18 +0000 @@ -78,7 +78,7 @@ switch (blit_format) { - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8: + case GRUB_VIDEO_BLIT_FORMAT_RGBA_8888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB | GRUB_VIDEO_MODE_TYPE_ALPHA; mode_info->bpp = 32; @@ -94,7 +94,7 @@ mode_info->reserved_field_pos = 24; break; - case GRUB_VIDEO_BLIT_FORMAT_R8G8B8: + case GRUB_VIDEO_BLIT_FORMAT_RGB_888: mode_info->mode_type = GRUB_VIDEO_MODE_TYPE_RGB; mode_info->bpp = 24; mode_info->bytes_per_pixel = 3; === added file 'video/bitmap_scale.c' --- video/bitmap_scale.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale.c 2008-08-03 01:30:02 +0000 @@ -0,0 +1,101 @@ +/* bitmap_scale.c - Bitmap scaling interface. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * This function creates a new scaled version of the bitmap SRC. The new + * bitmap has dimensions DST_WIDTH by DST_HEIGHT. The scaling algorithm + * is given by SCALE_METHOD. If an error is encountered, the return code is + * not equal to GRUB_ERR_NONE, and the bitmap DST is either not created, or + * it is destroyed before this function returns. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_create_scaled (struct grub_video_bitmap **dst, + int dst_width, int dst_height, + struct grub_video_bitmap *src, + enum grub_video_bitmap_scale_method + scale_method) +{ + *dst = 0; + + /* Verify the simplifying assumptions. */ + if (src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "null src bitmap in grub_video_bitmap_create_scaled"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "src format not supported for scale"); + if (src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + if (dst_width <= 0 || dst_height <= 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "requested to scale to a size w/ a zero dimension"); + if (src->mode_info.bytes_per_pixel * 8 != src->mode_info.bpp) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "bitmap to scale has inconsistent Bpp and bpp"); + + /* Create the new bitmap. */ + grub_err_t ret; + ret = grub_video_bitmap_create (dst, dst_width, dst_height, + src->mode_info.blit_format); + if (ret != GRUB_ERR_NONE) + return ret; /* Error. */ + + switch (scale_method) + { + case GRUB_VIDEO_BITMAP_SCALE_METHOD_FASTEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_NEAREST: + ret = grub_video_bitmap_scale_nn (*dst, src); + break; + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BEST: + case GRUB_VIDEO_BITMAP_SCALE_METHOD_BILINEAR: + ret = grub_video_bitmap_scale_bilinear (*dst, src); + break; + default: + ret = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid scale_method value"); + break; + } + + if (ret == GRUB_ERR_NONE) + { + /* Success: *dst is now a pointer to the scaled bitmap. */ + return GRUB_ERR_NONE; + } + else + { + /* Destroy the bitmap and return the error code. */ + grub_video_bitmap_destroy (*dst); + *dst = 0; + return ret; + } +} === added file 'video/bitmap_scale_bilinear.c' --- video/bitmap_scale_bilinear.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_bilinear.c 2008-07-03 14:28:15 +0000 @@ -0,0 +1,145 @@ +/* bitmap_scale_bilinear.c - Bilinear image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the bilinear interpolation algorithm + * to interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_bilinear (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale func"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* If we have enough space to do so, use bilinear interpolation. + * Otherwise, fall back to nearest neighbor for this pixel. */ + if (sx < sw - 1 && sy < sh - 1) + { + /* Do bilinear interpolation. */ + + /* Fixed-point .8 numbers representing the fraction of the + * distance in the x (u) and y (v) direction within the + * box of 4 pixels in the source. */ + int u = (256 * sw * dx / dw) - (sx * 256); + int v = (256 * sh * dy / dh) - (sy * 256); + + for (comp = 0; comp < bytes_per_pixel; comp++) + { + /* Get the component's values for the + * 4 source corner pixels. */ + grub_uint8_t f00 = sptr[comp]; + grub_uint8_t f10 = sptr[comp + bytes_per_pixel]; + grub_uint8_t f01 = sptr[comp + sstride]; + grub_uint8_t f11 = sptr[comp + sstride + bytes_per_pixel]; + + /* Do linear interpolations along the top and bottom + * rows of the box. */ + grub_uint8_t f0y = (256 - v) * f00 / 256 + v * f01 / 256; + grub_uint8_t f1y = (256 - v) * f10 / 256 + v * f11 / 256; + + /* Interpolate vertically. */ + grub_uint8_t fxy = (256 - u) * f0y / 256 + u * f1y / 256; + + dptr[comp] = fxy; + } + } + else + { + /* Fall back to nearest neighbor interpolation. */ + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + } + return GRUB_ERR_NONE; +} === added file 'video/bitmap_scale_nn.c' --- video/bitmap_scale_nn.c 1970-01-01 00:00:00 +0000 +++ video/bitmap_scale_nn.c 2008-07-03 14:27:43 +0000 @@ -0,0 +1,109 @@ +/* bitmap_scale_nn.c - Nearest neighbor image scaling algorithm. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +/* + * Copy the bitmap SRC to the bitmap DST, scaling the bitmap to fit the + * dimensions of DST. This function uses the nearest neighbor algorithm to + * interpolate the pixels. + * + * Supports only direct color modes which have components separated + * into bytes (e.g., RGBA 8:8:8:8 or BGR 8:8:8 true color). + * But because of this simplifying assumption, the implementation is + * greatly simplified. + */ +grub_err_t +grub_video_bitmap_scale_nn (struct grub_video_bitmap *dst, + struct grub_video_bitmap *src) +{ + /* Verify the simplifying assumptions. */ + if (dst == 0 || src == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "null bitmap in scale_nn"); + if (dst->mode_info.red_field_pos % 8 != 0 + || dst->mode_info.green_field_pos % 8 != 0 + || dst->mode_info.blue_field_pos % 8 != 0 + || dst->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst format not supported"); + if (src->mode_info.red_field_pos % 8 != 0 + || src->mode_info.green_field_pos % 8 != 0 + || src->mode_info.blue_field_pos % 8 != 0 + || src->mode_info.reserved_field_pos % 8 != 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "src format not supported"); + if (dst->mode_info.red_field_pos != src->mode_info.red_field_pos + || dst->mode_info.red_mask_size != src->mode_info.red_mask_size + || dst->mode_info.green_field_pos != src->mode_info.green_field_pos + || dst->mode_info.green_mask_size != src->mode_info.green_mask_size + || dst->mode_info.blue_field_pos != src->mode_info.blue_field_pos + || dst->mode_info.blue_mask_size != src->mode_info.blue_mask_size + || dst->mode_info.reserved_field_pos != + src->mode_info.reserved_field_pos + || dst->mode_info.reserved_mask_size != + src->mode_info.reserved_mask_size) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.bytes_per_pixel != src->mode_info.bytes_per_pixel) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "dst and src not compatible"); + if (dst->mode_info.width == 0 || dst->mode_info.height == 0 + || src->mode_info.width == 0 || src->mode_info.height == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "bitmap has a zero dimension"); + + grub_uint8_t *ddata = dst->data; + grub_uint8_t *sdata = src->data; + int dw = dst->mode_info.width; + int dh = dst->mode_info.height; + int sw = src->mode_info.width; + int sh = src->mode_info.height; + int dstride = dst->mode_info.pitch; + int sstride = src->mode_info.pitch; + /* bytes_per_pixel is the same for both src and dst. */ + int bytes_per_pixel = dst->mode_info.bytes_per_pixel; + + int dy; + for (dy = 0; dy < dh; dy++) + { + int dx; + for (dx = 0; dx < dw; dx++) + { + grub_uint8_t *dptr; + grub_uint8_t *sptr; + int sx; + int sy; + int comp; + + /* Compute the source coordinate that the destination coordinate + * maps to. Note: sx/sw = dx/dw => sx = sw*dx/dw. */ + sx = sw * dx / dw; + sy = sh * dy / dh; + + /* Get the address of the pixels in src and dst. */ + dptr = ddata + dy * dstride + dx * bytes_per_pixel; + sptr = sdata + sy * sstride + sx * bytes_per_pixel; + + /* Copy the pixel color value. */ + for (comp = 0; comp < bytes_per_pixel; comp++) + dptr[comp] = sptr[comp]; + } + } + return GRUB_ERR_NONE; +} === modified file 'video/i386/pc/vbe.c' --- video/i386/pc/vbe.c 2008-01-21 15:48:27 +0000 +++ video/i386/pc/vbe.c 2008-07-19 19:42:37 +0000 @@ -63,6 +63,7 @@ static struct { struct grub_video_render_target render_target; + int is_double_buffered; /* Is the video mode double buffered? */ unsigned int bytes_per_scan_line; unsigned int bytes_per_pixel; @@ -77,6 +78,24 @@ static grub_uint32_t mode_in_use = 0x55aa; static grub_uint16_t *mode_list; +static struct +{ + grub_size_t page_size; /* The size of a page in bytes. */ + + /* For page flipping strategy. */ + int displayed_page; /* The page # that is the front buffer. */ + int render_page; /* The page # that is the back buffer. */ + + /* For blit strategy. */ + grub_uint8_t *offscreen_buffer; + + /* Virtual functions. */ + int (*update_screen) (void); + int (*destroy) (void); +} doublebuf_state; + +static void double_buffering_init (void); + static void * real2pm (grub_vbe_farptr_t ptr) { @@ -376,6 +395,7 @@ /* Reset frame buffer and render target variables. */ grub_memset (&framebuffer, 0, sizeof(framebuffer)); render_target = &framebuffer.render_target; + grub_memset (&doublebuf_state, 0, sizeof(doublebuf_state)); return GRUB_ERR_NONE; } @@ -391,6 +411,9 @@ /* TODO: Decide, is this something we want to do. */ return grub_errno; + if (doublebuf_state.destroy) + doublebuf_state.destroy(); + /* TODO: Free any resources allocated by driver. */ grub_free (mode_list); mode_list = 0; @@ -533,9 +556,13 @@ render_target->viewport.width = active_mode_info.x_resolution; render_target->viewport.height = active_mode_info.y_resolution; - /* Set framebuffer pointer and mark it as non allocated. */ + /* Mark framebuffer memory as non allocated. */ render_target->is_allocated = 0; - render_target->data = framebuffer.ptr; + + /* Set up double buffering information. */ + framebuffer.is_double_buffered = + ((mode_type & GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED) != 0); + double_buffering_init (); /* Copy default palette to initialize emulated palette. */ for (i = 0; @@ -556,6 +583,166 @@ return grub_error (GRUB_ERR_UNKNOWN_DEVICE, "no matching mode found."); } +/* + Set framebuffer render target page and display the proper page, based on + `doublebuf_state.render_page' and `doublebuf_state.displayed_page', + respectively. + + Returns 0 upon success, nonzero upon failure. + */ +static int +doublebuf_pageflipping_commit (void) +{ + /* Set the render target's data pointer to the start of the render_page. */ + framebuffer.render_target.data = + ((char *) framebuffer.ptr) + + doublebuf_state.page_size * doublebuf_state.render_page; + + /* Tell the video adapter to display the new front page. */ + int display_start_line = + framebuffer.render_target.mode_info.height + * doublebuf_state.displayed_page; + + grub_vbe_status_t vbe_err = + grub_vbe_bios_set_display_start (0, display_start_line); + if (vbe_err != GRUB_VBE_STATUS_OK) + return 1; + + return 0; +} + +static int +doublebuf_pageflipping_update_screen (void) +{ + /* Swap the page numbers in the framebuffer struct. */ + int new_displayed_page = doublebuf_state.render_page; + doublebuf_state.render_page = doublebuf_state.displayed_page; + doublebuf_state.displayed_page = new_displayed_page; + + return doublebuf_pageflipping_commit (); +} + +static int +doublebuf_pageflipping_destroy (void) +{ + doublebuf_state.update_screen = 0; + doublebuf_state.destroy = 0; + return 0; +} + +static int +doublebuf_pageflipping_init (void) +{ + doublebuf_state.page_size = + framebuffer.bytes_per_scan_line * render_target->mode_info.height; + + /* Get video RAM size in bytes. */ + grub_size_t vram_size = controller_info.total_memory << 16; + + if (2 * doublebuf_state.page_size > vram_size) + return 1; /* Not enough video memory for 2 pages. */ + + doublebuf_state.displayed_page = 0; + doublebuf_state.render_page = 1; + + doublebuf_state.update_screen = doublebuf_pageflipping_update_screen; + doublebuf_state.destroy = doublebuf_pageflipping_destroy; + + /* Set the framebuffer memory data pointer and display the right page. */ + if (doublebuf_pageflipping_commit () != GRUB_ERR_NONE) + return 1; /* Unable to set the display start. */ + + framebuffer.render_target.mode_info.mode_type + |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + return 0; +} + +static int +doublebuf_blit_update_screen (void) +{ + grub_memcpy (framebuffer.ptr, + doublebuf_state.offscreen_buffer, + doublebuf_state.page_size); + return 0; +} + +static int +doublebuf_blit_destroy (void) +{ + grub_free (doublebuf_state.offscreen_buffer); + doublebuf_state.offscreen_buffer = 0; + + doublebuf_state.update_screen = 0; + doublebuf_state.destroy = 0; + return 0; +} + +static int +doublebuf_blit_init (void) +{ + doublebuf_state.page_size = + framebuffer.bytes_per_scan_line * render_target->mode_info.height; + + doublebuf_state.offscreen_buffer = (grub_uint8_t *) + grub_malloc (doublebuf_state.page_size); + if (doublebuf_state.offscreen_buffer == 0) + return 1; /* Error. */ + + framebuffer.render_target.data = doublebuf_state.offscreen_buffer; + doublebuf_state.update_screen = doublebuf_blit_update_screen; + doublebuf_state.destroy = doublebuf_blit_destroy; + + framebuffer.render_target.mode_info.mode_type + |= GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + return 0; +} + +static int +doublebuf_null_update_screen (void) +{ + return 0; +} + +static int +doublebuf_null_destroy (void) +{ + doublebuf_state.update_screen = 0; + doublebuf_state.destroy = 0; + return 0; +} + +static int +doublebuf_null_init (void) +{ + framebuffer.render_target.data = framebuffer.ptr; + doublebuf_state.update_screen = doublebuf_null_update_screen; + doublebuf_state.destroy = doublebuf_null_destroy; + + framebuffer.render_target.mode_info.mode_type + &= ~GRUB_VIDEO_MODE_TYPE_DOUBLE_BUFFERED; + return 0; +} + +/* Select the best double buffering mode available. */ +static void +double_buffering_init (void) +{ + if (doublebuf_state.destroy) + doublebuf_state.destroy(); + + if (framebuffer.is_double_buffered) + { + if (doublebuf_pageflipping_init () == 0) + return; + + if (doublebuf_blit_init () == 0) + return; + } + + /* Fall back to no double buffering. */ + doublebuf_null_init (); +} + static grub_err_t grub_video_vbe_get_info (struct grub_video_mode_info *mode_info) { @@ -708,6 +895,16 @@ return minindex; } + else if ((render_target->mode_info.mode_type + & GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP) != 0) + { + if (red == render_target->mode_info.fg_red + && green == render_target->mode_info.fg_green + && blue == render_target->mode_info.fg_blue) + return 1; + else + return 0; + } else { grub_uint32_t value; @@ -737,6 +934,17 @@ /* No alpha available in index color modes, just use same value as in only RGB modes. */ return grub_video_vbe_map_rgb (red, green, blue); + else if ((render_target->mode_info.mode_type + & GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP) != 0) + { + if (red == render_target->mode_info.fg_red + && green == render_target->mode_info.fg_green + && blue == render_target->mode_info.fg_blue + && alpha == render_target->mode_info.fg_alpha) + return 1; + else + return 0; + } else { grub_uint32_t value; @@ -797,6 +1005,24 @@ *alpha = framebuffer.palette[color].a; return; } + else if ((mode_info->mode_type + & GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP) != 0) + { + if (color & 1) + { + *red = mode_info->fg_red; + *green = mode_info->fg_green; + *blue = mode_info->fg_blue; + *alpha = mode_info->fg_alpha; + } + else + { + *red = mode_info->bg_red; + *green = mode_info->bg_green; + *blue = mode_info->bg_blue; + *alpha = mode_info->bg_alpha; + } + } else { grub_uint32_t tmp; @@ -876,17 +1102,24 @@ target.data = render_target->data; /* Try to figure out more optimized version. */ - if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbefill_R8G8B8A8 (&target, color, x, y, - width, height); - return GRUB_ERR_NONE; - } - - if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbefill_R8G8B8 (&target, color, x, y, - width, height); + if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbefill_direct32 (&target, color, x, y, + width, height); + return GRUB_ERR_NONE; + } + + if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbefill_direct32 (&target, color, x, y, + width, height); + return GRUB_ERR_NONE; + } + + if (target.mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbefill_direct24 (&target, color, x, y, + width, height); return GRUB_ERR_NONE; } @@ -903,76 +1136,6 @@ return GRUB_ERR_NONE; } -// TODO: Remove this method and replace with bitmap based glyphs -static grub_err_t -grub_video_vbe_blit_glyph (struct grub_font_glyph * glyph, - grub_video_color_t color, int x, int y) -{ - struct grub_video_i386_vbeblit_info target; - unsigned int width; - unsigned int charwidth; - unsigned int height; - unsigned int i; - unsigned int j; - unsigned int x_offset = 0; - unsigned int y_offset = 0; - - /* Make sure there is something to do. */ - if (x >= (int)render_target->viewport.width) - return GRUB_ERR_NONE; - - if (y >= (int)render_target->viewport.height) - return GRUB_ERR_NONE; - - /* Calculate glyph dimensions. */ - width = ((glyph->width + 7) / 8) * 8; - charwidth = width; - height = glyph->height; - - if (x + (int)width < 0) - return GRUB_ERR_NONE; - - if (y + (int)height < 0) - return GRUB_ERR_NONE; - - /* Do not allow drawing out of viewport. */ - if (x < 0) - { - width += x; - x_offset = (unsigned int)-x; - x = 0; - } - if (y < 0) - { - height += y; - y_offset = (unsigned int)-y; - y = 0; - } - - if ((x + width) > render_target->viewport.width) - width = render_target->viewport.width - x; - if ((y + height) > render_target->viewport.height) - height = render_target->viewport.height - y; - - /* Add viewport offset. */ - x += render_target->viewport.x; - y += render_target->viewport.y; - - /* Use vbeblit_info to encapsulate rendering. */ - target.mode_info = &render_target->mode_info; - target.data = render_target->data; - - /* Draw glyph. */ - for (j = 0; j < height; j++) - for (i = 0; i < width; i++) - if ((glyph->bitmap[((i + x_offset) / 8) - + (j + y_offset) * (charwidth / 8)] - & (1 << ((charwidth - (i + x_offset) - 1) % 8)))) - set_pixel (&target, x+i, y+j, color); - - return GRUB_ERR_NONE; -} - /* NOTE: This function assumes that given coordinates are within bounds of handled data. */ static void @@ -985,19 +1148,35 @@ if (oper == GRUB_VIDEO_BLIT_REPLACE) { /* Try to figure out more optimized version for replace operator. */ - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbeblit_R8G8B8X8_R8G8B8X8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbeblit_direct32_copy (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRX8888_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; } - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGR_888) + { + grub_video_i386_vbeblit_BGR888_RGBX8888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbeblit_RGB888_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1005,26 +1184,42 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8X8 (target, source, + grub_video_i386_vbeblit_index_RGBX8888 (target, source, x, y, width, height, offset_x, offset_y); return; } } - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (target, source, - x, y, width, height, - offset_x, offset_y); - return; - } - - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbeblit_R8G8B8_R8G8B8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRA8888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbeblit_RGBA8888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGR_888) + { + grub_video_i386_vbeblit_BGR888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbeblit_RGB888_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1032,13 +1227,24 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8 (target, source, + grub_video_i386_vbeblit_index_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; } } + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_direct32_copy (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + } + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) @@ -1057,19 +1263,35 @@ else { /* Try to figure out more optimized blend operator. */ - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (target, source, - x, y, width, height, - offset_x, offset_y); - return; - } - - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbeblit_R8G8B8_R8G8B8A8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRA8888_RGBA8888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbeblit_RGBA8888_RGBA8888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGR_888) + { + grub_video_i386_vbeblit_BGR888_RGBA8888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbeblit_RGB888_RGBA8888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1077,26 +1299,42 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8A8 (target, source, + grub_video_i386_vbeblit_index_RGBA8888 (target, source, x, y, width, height, offset_x, offset_y); return; } } - if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) { - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8) - { - grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (target, source, - x, y, width, height, - offset_x, offset_y); - return; - } - - if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_R8G8B8) - { - grub_video_i386_vbeblit_R8G8B8_R8G8B8 (target, source, + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGRA_8888) + { + grub_video_i386_vbeblit_BGRA8888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGBA_8888) + { + grub_video_i386_vbeblit_RGBA8888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_BGR_888) + { + grub_video_i386_vbeblit_BGR888_RGB888 (target, source, + x, y, width, height, + offset_x, offset_y); + return; + } + + if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_RGB_888) + { + grub_video_i386_vbeblit_RGB888_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1104,7 +1342,7 @@ if (target->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_INDEXCOLOR) { - grub_video_i386_vbeblit_index_R8G8B8 (target, source, + grub_video_i386_vbeblit_index_RGB888 (target, source, x, y, width, height, offset_x, offset_y); return; @@ -1219,6 +1457,77 @@ return GRUB_ERR_NONE; } +/* + * Draw the specified glyph at (x, y). The y coordinate designates the + * baseline of the character, while the x coordinate designates the left + * side location of the character. + */ +static grub_err_t +grub_video_vbe_blit_glyph (struct grub_font_glyph *glyph, + grub_video_color_t color, + int left_x, int baseline_y) +{ + struct grub_video_bitmap glyph_bitmap; + + /* Don't try to draw empty glyphs (U+0020, etc.). */ + if (glyph->width == 0 || glyph->height == 0) + return GRUB_ERR_NONE; + + glyph_bitmap.mode_info.width = glyph->width; + glyph_bitmap.mode_info.height = glyph->height; + glyph_bitmap.mode_info.mode_type = + (1 << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) + | GRUB_VIDEO_MODE_TYPE_1BIT_BITMAP; + glyph_bitmap.mode_info.blit_format = GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED; + glyph_bitmap.mode_info.bpp = 1; + glyph_bitmap.mode_info.bytes_per_pixel = 0; /* Really 1 bit per pixel. */ + glyph_bitmap.mode_info.pitch = glyph->width; /* Packed densely as bits. */ + glyph_bitmap.mode_info.number_of_colors = 2; + glyph_bitmap.mode_info.bg_red = 0; + glyph_bitmap.mode_info.bg_green = 0; + glyph_bitmap.mode_info.bg_blue = 0; + glyph_bitmap.mode_info.bg_alpha = 0; + grub_video_vbe_unmap_color(color, + &glyph_bitmap.mode_info.fg_red, + &glyph_bitmap.mode_info.fg_green, + &glyph_bitmap.mode_info.fg_blue, + &glyph_bitmap.mode_info.fg_alpha); + glyph_bitmap.data = glyph->bitmap; + + int bitmap_left = left_x + glyph->offset_x; + int bitmap_bottom = baseline_y - glyph->offset_y; + int bitmap_top = bitmap_bottom - glyph->height; + + return grub_video_vbe_blit_bitmap (&glyph_bitmap, GRUB_VIDEO_BLIT_BLEND, + bitmap_left, bitmap_top, + 0, 0, + glyph->width, glyph->height); +} + +static grub_err_t +grub_video_vbe_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y) +{ + grub_size_t len; + grub_size_t i; + int x; + struct grub_font_glyph *glyph; + + len = grub_strlen (str); + x = left_x; + for (i = 0; i < len; i++) + { + glyph = grub_font_get_glyph (font, str[i]); + if (grub_video_vbe_blit_glyph (glyph, color, x, baseline_y) + != GRUB_ERR_NONE) + return grub_errno; + x += glyph->device_width; + } + + return GRUB_ERR_NONE; +} + static grub_err_t grub_video_vbe_blit_render_target (struct grub_video_render_target *source, enum grub_video_blit_operators oper, @@ -1403,10 +1712,13 @@ return GRUB_ERR_NONE; } -static grub_err_t +static grub_err_t grub_video_vbe_swap_buffers (void) { - /* TODO: Implement buffer swapping. */ + if (doublebuf_state.update_screen () != 0) + return grub_error (GRUB_ERR_INVALID_COMMAND, + "Double buffer update failed"); + return GRUB_ERR_NONE; } @@ -1505,17 +1817,13 @@ static grub_err_t grub_video_vbe_set_active_render_target (struct grub_video_render_target *target) { - if (target == GRUB_VIDEO_RENDER_TARGET_FRONT_BUFFER) + if (target == GRUB_VIDEO_RENDER_TARGET_DISPLAY) { render_target = &framebuffer.render_target; - + return GRUB_ERR_NONE; } - if (target == GRUB_VIDEO_RENDER_TARGET_BACK_BUFFER) - return grub_error (GRUB_ERR_NOT_IMPLEMENTED_YET, - "double buffering not implemented yet."); - if (! target->data) return grub_error (GRUB_ERR_BAD_ARGUMENT, "invalid render target given."); @@ -1551,6 +1859,7 @@ .unmap_color = grub_video_vbe_unmap_color, .fill_rect = grub_video_vbe_fill_rect, .blit_glyph = grub_video_vbe_blit_glyph, + .draw_string = grub_video_vbe_draw_string, .blit_bitmap = grub_video_vbe_blit_bitmap, .blit_render_target = grub_video_vbe_blit_render_target, .scroll = grub_video_vbe_scroll, === modified file 'video/i386/pc/vbeblit.c' --- video/i386/pc/vbeblit.c 2008-01-01 12:02:07 +0000 +++ video/i386/pc/vbeblit.c 2008-07-09 17:27:53 +0000 @@ -35,7 +35,343 @@ #include void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_BGRX8888_RGBX8888 (struct grub_video_i386_vbeblit_info + *dst, + struct grub_video_i386_vbeblit_info + *src, int x, int y, int width, + int height, int offset_x, + int offset_y) +{ + int i; + int j; + grub_uint8_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint8_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint8_t r = *srcptr++; + grub_uint8_t g = *srcptr++; + grub_uint8_t b = *srcptr++; + grub_uint8_t a = *srcptr++; + + *dstptr++ = b; + *dstptr++ = g; + *dstptr++ = r; + *dstptr++ = a; + } + srcptr += srcrowskip; + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGRA8888_RGB888 (struct grub_video_i386_vbeblit_info + *dst, + struct grub_video_i386_vbeblit_info + *src, int x, int y, int width, + int height, int offset_x, + int offset_y) +{ + int i; + int j; + grub_uint8_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint8_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint8_t r = *srcptr++; + grub_uint8_t g = *srcptr++; + grub_uint8_t b = *srcptr++; + + *dstptr++ = b; + *dstptr++ = g; + *dstptr++ = r; + *dstptr++ = 255; /* Alpha component: Set opaque. */ + } + srcptr += srcrowskip; + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGR888_RGB888 (struct grub_video_i386_vbeblit_info + *dst, + struct grub_video_i386_vbeblit_info + *src, int x, int y, int width, + int height, int offset_x, + int offset_y) +{ + int i; + int j; + grub_uint8_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint8_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint8_t r = *srcptr++; + grub_uint8_t g = *srcptr++; + grub_uint8_t b = *srcptr++; + + *dstptr++ = b; + *dstptr++ = g; + *dstptr++ = r; + } + srcptr += srcrowskip; + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGRA8888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y) +{ + grub_uint32_t *srcptr; + grub_uint32_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + int i; + int j; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint32_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint32_t *) get_data_ptr (dst, x, y); + + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint32_t color; + unsigned int sr; + unsigned int sg; + unsigned int sb; + unsigned int a; + unsigned int dr; + unsigned int dg; + unsigned int db; + + color = *srcptr++; + + a = color >> 24; + + if (a == 0) + { + /* Skip transparent source pixels. */ + dstptr++; + continue; + } + + sr = (color >> 0) & 0xFF; + sg = (color >> 8) & 0xFF; + sb = (color >> 16) & 0xFF; + + if (a == 255) + { + /* Opaque pixel shortcut. */ + dr = sr; + dg = sg; + db = sb; + } + else + { + /* General pixel color blending. */ + color = *dstptr; + + dr = (color >> 16) & 0xFF; + dr = (dr * (255 - a) + sr * a) / 255; + dg = (color >> 8) & 0xFF; + dg = (dg * (255 - a) + sg * a) / 255; + db = (color >> 0) & 0xFF; + db = (db * (255 - a) + sb * a) / 255; + } + + color = (a << 24) | (dr << 16) | (dg << 8) | db; + + *dstptr++ = color; + } + + srcptr = (grub_uint32_t *) (((grub_uint8_t *) srcptr) + srcrowskip); + dstptr = (grub_uint32_t *) (((grub_uint8_t *) dstptr) + dstrowskip); + } +} + +void +grub_video_i386_vbeblit_BGR888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y) +{ + grub_uint32_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + int i; + int j; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint32_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint32_t color; + unsigned int sr; + unsigned int sg; + unsigned int sb; + unsigned int a; + unsigned int dr; + unsigned int dg; + unsigned int db; + + color = *srcptr++; + + a = color >> 24; + + if (a == 0) + { + /* Skip transparent source pixels. */ + dstptr += 3; + continue; + } + + sr = (color >> 0) & 0xFF; + sg = (color >> 8) & 0xFF; + sb = (color >> 16) & 0xFF; + + if (a == 255) + { + /* Opaque pixel shortcut. */ + dr = sr; + dg = sg; + db = sb; + } + else + { + /* General pixel color blending. */ + color = *dstptr; + + db = dstptr[0]; + db = (db * (255 - a) + sb * a) / 255; + dg = dstptr[1]; + dg = (dg * (255 - a) + sg * a) / 255; + dr = dstptr[2]; + dr = (dr * (255 - a) + sr * a) / 255; + } + + *dstptr++ = db; + *dstptr++ = dg; + *dstptr++ = dr; + } + + srcptr = (grub_uint32_t *) (((grub_uint8_t *) srcptr) + srcrowskip); + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_BGR888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y) +{ + grub_uint32_t *srcptr; + grub_uint8_t *dstptr; + unsigned srcrowskip; + unsigned dstrowskip; + int i; + int j; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + srcrowskip = + src->mode_info->pitch - src->mode_info->bytes_per_pixel * width; + dstrowskip = + dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + srcptr = (grub_uint32_t *) get_data_ptr (src, offset_x, offset_y); + dstptr = (grub_uint8_t *) get_data_ptr (dst, x, y); + + for (j = 0; j < height; j++) + { + for (i = 0; i < width; i++) + { + grub_uint32_t color; + grub_uint8_t sr; + grub_uint8_t sg; + grub_uint8_t sb; + + color = *srcptr++; + + sr = (color >> 0) & 0xFF; + sg = (color >> 8) & 0xFF; + sb = (color >> 16) & 0xFF; + + *dstptr++ = sb; + *dstptr++ = sg; + *dstptr++ = sr; + } + + srcptr = (grub_uint32_t *) (((grub_uint8_t *) srcptr) + srcrowskip); + dstptr += dstrowskip; + } +} + +void +grub_video_i386_vbeblit_RGBA8888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -101,10 +437,10 @@ } void -grub_video_i386_vbeblit_R8G8B8X8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, - struct grub_video_i386_vbeblit_info *src, - int x, int y, int width, int height, - int offset_x, int offset_y) +grub_video_i386_vbeblit_direct32_copy (struct grub_video_i386_vbeblit_info *dst, + struct grub_video_i386_vbeblit_info *src, + int x, int y, int width, int height, + int offset_x, int offset_y) { int j; grub_uint32_t *srcptr; @@ -126,7 +462,7 @@ } void -grub_video_i386_vbeblit_R8G8B8_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGB888_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -193,7 +529,7 @@ } void -grub_video_i386_vbeblit_R8G8B8_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGB888_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -231,7 +567,7 @@ } void -grub_video_i386_vbeblit_index_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_index_RGBA8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -295,7 +631,7 @@ } void -grub_video_i386_vbeblit_index_R8G8B8X8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_index_RGBX8888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -332,7 +668,7 @@ } void -grub_video_i386_vbeblit_R8G8B8A8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGBA8888_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -368,7 +704,7 @@ } void -grub_video_i386_vbeblit_R8G8B8_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_RGB888_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) @@ -393,7 +729,7 @@ } void -grub_video_i386_vbeblit_index_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbeblit_index_RGB888 (struct grub_video_i386_vbeblit_info *dst, struct grub_video_i386_vbeblit_info *src, int x, int y, int width, int height, int offset_x, int offset_y) === modified file 'video/i386/pc/vbefill.c' --- video/i386/pc/vbefill.c 2007-07-21 22:32:33 +0000 +++ video/i386/pc/vbefill.c 2008-07-03 13:49:18 +0000 @@ -34,51 +34,63 @@ #include void -grub_video_i386_vbefill_R8G8B8A8 (struct grub_video_i386_vbeblit_info *dst, +grub_video_i386_vbefill_direct32 (struct grub_video_i386_vbeblit_info *dst, grub_video_color_t color, int x, int y, int width, int height) { int i; int j; grub_uint32_t *dstptr; - - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ + grub_size_t rowskip; + + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + rowskip = dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + /* Get the start address. */ + dstptr = (grub_uint32_t *) grub_video_vbe_get_video_ptr (dst, x, y); for (j = 0; j < height; j++) { - dstptr = (grub_uint32_t *)grub_video_vbe_get_video_ptr (dst, x, y + j); - for (i = 0; i < width; i++) *dstptr++ = color; + + /* Advance the dest pointer to the right location on the next line. */ + dstptr = (grub_uint32_t *) (((char *) dstptr) + rowskip); } } void -grub_video_i386_vbefill_R8G8B8 (struct grub_video_i386_vbeblit_info *dst, - grub_video_color_t color, int x, int y, - int width, int height) +grub_video_i386_vbefill_direct24 (struct grub_video_i386_vbeblit_info *dst, + grub_video_color_t color, int x, int y, + int width, int height) { int i; int j; + grub_size_t rowskip; grub_uint8_t *dstptr; - grub_uint8_t fillr = (grub_uint8_t)((color >> 0) & 0xFF); - grub_uint8_t fillg = (grub_uint8_t)((color >> 8) & 0xFF); - grub_uint8_t fillb = (grub_uint8_t)((color >> 16) & 0xFF); + grub_uint8_t fill0 = (grub_uint8_t)((color >> 0) & 0xFF); + grub_uint8_t fill1 = (grub_uint8_t)((color >> 8) & 0xFF); + grub_uint8_t fill2 = (grub_uint8_t)((color >> 16) & 0xFF); - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + rowskip = dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + /* Get the start address. */ + dstptr = (grub_uint8_t *) grub_video_vbe_get_video_ptr (dst, x, y); for (j = 0; j < height; j++) { - dstptr = (grub_uint8_t *)grub_video_vbe_get_video_ptr (dst, x, y + j); - for (i = 0; i < width; i++) { - *dstptr++ = fillr; - *dstptr++ = fillg; - *dstptr++ = fillb; + *dstptr++ = fill0; + *dstptr++ = fill1; + *dstptr++ = fill2; } + + /* Advance the dest pointer to the right location on the next line. */ + dstptr += rowskip; } } @@ -89,18 +101,24 @@ { int i; int j; + grub_size_t rowskip; grub_uint8_t *dstptr; grub_uint8_t fill = (grub_uint8_t)color & 0xFF; - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ + /* Calculate the number of bytes to advance from the end of one line + * to the beginning of the next line. */ + rowskip = dst->mode_info->pitch - dst->mode_info->bytes_per_pixel * width; + + /* Get the start address. */ + dstptr = (grub_uint8_t *) grub_video_vbe_get_video_ptr (dst, x, y); for (j = 0; j < height; j++) { - dstptr = (grub_uint8_t *)grub_video_vbe_get_video_ptr (dst, x, y + j); - for (i = 0; i < width; i++) *dstptr++ = fill; + + /* Advance the dest pointer to the right location on the next line. */ + dstptr += rowskip; } } @@ -112,9 +130,6 @@ int i; int j; - /* We do not need to worry about data being out of bounds - as we assume that everything has been checked before. */ - for (j = 0; j < height; j++) for (i = 0; i < width; i++) set_pixel (dst, x+i, y+j, color); === modified file 'video/i386/pc/vbeutil.c' --- video/i386/pc/vbeutil.c 2007-07-21 22:32:33 +0000 +++ video/i386/pc/vbeutil.c 2008-07-03 14:10:25 +0000 @@ -52,6 +52,11 @@ + y * source->mode_info->pitch + x; break; + + /* case 1: */ + /* For 1-bit bitmaps, addressing needs to be done at the bit level + * and it doesn't make sense, in general, to ask for a pointer + * to a particular pixel's data. */ } return ptr; @@ -86,6 +91,17 @@ color = *(grub_uint8_t *)get_data_ptr (source, x, y); break; + case 1: + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED) + { + int bit_index = y * source->mode_info->width + x; + grub_uint8_t *ptr = (grub_uint8_t *)source->data + + bit_index / 8; + int bit_pos = 7 - bit_index % 8; + color = (*ptr >> bit_pos) & 0x01; + } + break; + default: break; } @@ -143,6 +159,17 @@ } break; + case 1: + if (source->mode_info->blit_format == GRUB_VIDEO_BLIT_FORMAT_1BIT_PACKED) + { + int bit_index = y * source->mode_info->width + x; + grub_uint8_t *ptr = (grub_uint8_t *)source->data + + bit_index / 8; + int bit_pos = 7 - bit_index % 8; + *ptr = (*ptr & ~(1 << bit_pos)) | ((color & 0x01) << bit_pos); + } + break; + default: break; } === modified file 'video/readers/jpeg.c' --- video/readers/jpeg.c 2008-08-01 03:06:55 +0000 +++ video/readers/jpeg.c 2008-08-03 03:57:46 +0000 @@ -1,20 +1,20 @@ /* - * GRUB -- GRand Unified Bootloader - * Copyright (C) 2008 Free Software Foundation, Inc. - * - * GRUB is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * GRUB is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with GRUB. If not, see . - */ +* GRUB -- GRand Unified Bootloader +* Copyright (C) 2008 Free Software Foundation, Inc. +* +* GRUB is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GRUB is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GRUB. If not, see . +*/ #include #include @@ -542,7 +542,7 @@ if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8)) + GRUB_VIDEO_BLIT_FORMAT_RGB_888)) return grub_errno; data->bit_mask = 0x0; === modified file 'video/readers/png.c' --- video/readers/png.c 2008-08-01 03:06:55 +0000 +++ video/readers/png.c 2008-08-03 04:00:22 +0000 @@ -231,7 +231,7 @@ { if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8)) + GRUB_VIDEO_BLIT_FORMAT_RGB_888)) return grub_errno; data->bpp = 3; } @@ -239,7 +239,7 @@ { if (grub_video_bitmap_create (data->bitmap, data->image_width, data->image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8)) + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888)) return grub_errno; data->bpp = 4; } === modified file 'video/readers/tga.c' --- video/readers/tga.c 2008-08-01 03:06:55 +0000 +++ video/readers/tga.c 2008-08-03 03:57:46 +0000 @@ -397,7 +397,7 @@ { grub_video_bitmap_create (bitmap, header.image_width, header.image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8); + GRUB_VIDEO_BLIT_FORMAT_RGBA_8888); if (grub_errno != GRUB_ERR_NONE) { grub_file_close (file); @@ -420,7 +420,7 @@ { grub_video_bitmap_create (bitmap, header.image_width, header.image_height, - GRUB_VIDEO_BLIT_FORMAT_R8G8B8); + GRUB_VIDEO_BLIT_FORMAT_RGB_888); if (grub_errno != GRUB_ERR_NONE) { grub_file_close (file); === added file 'video/setmode.c' --- video/setmode.c 1970-01-01 00:00:00 +0000 +++ video/setmode.c 2008-07-19 19:31:46 +0000 @@ -0,0 +1,249 @@ +/* video/setmode.c - Smart video mode selection based on preferences. */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2006,2007,2008 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +/* Set the video mode based on the preferred modes specified in MODE_LIST in + the form: x[x][;...] + + For example: 640x480;800x600x8;400x300x32 + + If MODE_LIST is null, or no modes in it are usable, then DEFAULT_WIDTH and + DEFAULT_HEIGHT are used to set the mode. The MODE_FLAGS argument determines + the video mode flags such as double buffering that are used. */ + +grub_err_t +grub_video_setup_preferred_mode (const char *mode_list, int mode_flags, + int default_width, int default_height) +{ + int mode_found = 0; + + if (mode_list != NULL) + { + /* Take copy of mode_list as we don't want tat. */ + char *const modes_copy = grub_strdup (mode_list); + if (modes_copy == NULL) + return grub_errno; + + /* Initialize next mode. */ + char *next_mode = modes_copy; + + /* Loop until all modes has been tested out. */ + while ((next_mode != NULL) && !mode_found) + { + /* Use last next_mode as current mode. */ + char *tmp = next_mode; + + int width = -1; + int height = -1; + int depth = -1; + + /* Save position of next mode and separate modes. */ + next_mode = grub_strchr(next_mode, ';'); + if (next_mode) + { + *next_mode = 0; + next_mode++; + } + + /* Skip whitespace. */ + while (grub_isspace (*tmp)) + tmp++; + + /* Initialize token holders. */ + char *current_mode = tmp; + char *param = tmp; + char *value = NULL; + + /* Parse x[x]*/ + + /* Find width value. */ + value = param; + param = grub_strchr(param, 'x'); + if (param == NULL) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + *param = 0; + param++; + + width = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + /* Find height value. */ + value = param; + param = grub_strchr(param, 'x'); + if (param == NULL) + { + height = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + } + else + { + /* We have optional color depth value. */ + *param = 0; + param++; + + height = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + + /* Convert color depth value. */ + value = param; + depth = grub_strtoul (value, 0, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_err_t rc; + + /* First setup error message. */ + rc = grub_error (GRUB_ERR_BAD_ARGUMENT, + "Invalid mode: %s\n", + current_mode); + + /* Free memory before returning. */ + grub_free (modes_copy); + + return rc; + } + } + + /* Try out video mode. */ + + int flags = mode_flags; + /* If we have <= 8 bits, assume it is an indexed color mode. */ + if ((depth <= 8) && (depth != -1)) + flags |= GRUB_VIDEO_MODE_TYPE_INDEX_COLOR; + + /* We have > 8 bits; assume that it is RGB color mode. */ + if (depth > 8) + flags |= GRUB_VIDEO_MODE_TYPE_RGB; + + /* If user requested specific depth, pass the request to driver. */ + if (depth != -1) + flags |= (depth << GRUB_VIDEO_MODE_TYPE_DEPTH_POS) + & GRUB_VIDEO_MODE_TYPE_DEPTH_MASK; + + /* Try to initialize requested mode. Ignore any errors. */ + grub_error_push (); + if (grub_video_setup (width, height, flags) != GRUB_ERR_NONE) + { + grub_error_pop (); + continue; + } + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + { + /* Couldn't get video mode info, restore old mode + and continue to next one. */ + grub_error_pop (); + + grub_video_restore (); + continue; + } + + /* Restore state of error stack. */ + grub_error_pop (); + + /* Mode found! Exit loop. */ + mode_found = 1; + } + + /* Free memory. */ + grub_free (modes_copy); + } + + if (!mode_found) + { + /* No gfxmode variable set, or no listed mode was supported. + Use the caller-specified defaults. */ + int flags = mode_flags | GRUB_VIDEO_MODE_TYPE_RGB; + + /* Initialize user requested mode. */ + if (grub_video_setup (default_width, default_height, flags) + != GRUB_ERR_NONE) + return grub_errno; + + /* Figure out what mode we ended up. */ + struct grub_video_mode_info mode_info; + if (grub_video_get_info (&mode_info) != GRUB_ERR_NONE) + grub_video_restore (); + else + mode_found = 1; + } + + if (!mode_found) + return grub_error (GRUB_ERR_BAD_ARGUMENT, + "No suitable mode found."); + + return (grub_errno = GRUB_ERR_NONE); +} === modified file 'video/video.c' --- video/video.c 2008-01-01 12:02:07 +0000 +++ video/video.c 2008-07-09 16:50:11 +0000 @@ -152,15 +152,22 @@ if (mode_info->bpp == 32) { if ((mode_info->red_mask_size == 8) + && (mode_info->red_field_pos == 16) + && (mode_info->green_mask_size == 8) + && (mode_info->green_field_pos == 8) + && (mode_info->blue_mask_size == 8) + && (mode_info->blue_field_pos == 0)) + { + return GRUB_VIDEO_BLIT_FORMAT_BGRA_8888; + } + if ((mode_info->red_mask_size == 8) && (mode_info->red_field_pos == 0) && (mode_info->green_mask_size == 8) && (mode_info->green_field_pos == 8) && (mode_info->blue_mask_size == 8) - && (mode_info->blue_field_pos == 16) - && (mode_info->reserved_mask_size == 8) - && (mode_info->reserved_field_pos == 24)) + && (mode_info->blue_field_pos == 16)) { - return GRUB_VIDEO_BLIT_FORMAT_R8G8B8A8; + return GRUB_VIDEO_BLIT_FORMAT_RGBA_8888; } } @@ -168,13 +175,22 @@ if (mode_info->bpp == 24) { if ((mode_info->red_mask_size == 8) + && (mode_info->red_field_pos == 16) + && (mode_info->green_mask_size == 8) + && (mode_info->green_field_pos == 8) + && (mode_info->blue_mask_size == 8) + && (mode_info->blue_field_pos == 0)) + { + return GRUB_VIDEO_BLIT_FORMAT_BGR_888; + } + if ((mode_info->red_mask_size == 8) && (mode_info->red_field_pos == 0) && (mode_info->green_mask_size == 8) && (mode_info->green_field_pos == 8) && (mode_info->blue_mask_size == 8) && (mode_info->blue_field_pos == 16)) { - return GRUB_VIDEO_BLIT_FORMAT_R8G8B8; + return GRUB_VIDEO_BLIT_FORMAT_RGB_888; } } @@ -305,6 +321,19 @@ return grub_video_adapter_active->blit_glyph (glyph, color, x, y); } +/* Draw string to screen using specified color and font. */ +grub_err_t +grub_video_draw_string (const char *str, grub_font_t font, + grub_video_color_t color, + int left_x, int baseline_y) +{ + if (! grub_video_adapter_active) + return grub_error (GRUB_ERR_BAD_DEVICE, "No video mode activated"); + + return grub_video_adapter_active->draw_string (str, font, color, + left_x, baseline_y); +} + /* Blit bitmap to screen. */ grub_err_t grub_video_blit_bitmap (struct grub_video_bitmap *bitmap, # Begin bundle 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 8VWK1NDiZEeBIvQSVBe6QQKjnqIJjOp3FngIHJUZe9ZQIKo73d4Iclg8Iqt+cpQ1iNgV6ugVERCw J3807thKEevIuaaJSvMnQTROeyrfVfFLJywLFivO+0jlX65iU4d+tY2qSq0ihknTOR1H1OdoziZp +59PpRrEdYeHKrvb4wQZyb5lt2lHvcgvIkmXey0ixphqObKfSXk0s4ZN3DQb6BzSmalc988ZmrPN kwxmyvDxvZuXYbDJro6eLMtyjOtdHTl45aM2HPjJVuuseKtmWtGyk8btzM5O59nfj5+6ce8mlEPS 0kS0Cs9O8uHDjGEGzkkVcBhRRpOcQ5E4ZqQXDU8IlwQuIiHCfewrVyI6dKO2Dqkp+3tsnTL87IlM jH0/OvL9GeaaIbKkfmzV1xsjlW5NdEm+IqJYvGxN15OlPXZs1T5y9fH6N2bjnJ/UnnUc/FGzNezK 8W5XnPjntjM65acJ791est1OGraQlKBKNL8xKcvnifY4f6siJcxRZ2ykSMCJwctzUpkRMzg3JD1E kUiakSJoRFJmEug+4m0WEVURmF5KOu03dhGlKsCqmVVVLYrq1U+J5HV9xLySOoG5lq2T5JWqRjCM JVMNm0pSkptlly5YdCGycVm5BPqkJJh5snMp3ciM5NSabpVK1SnG3b8bnDSRNXF/jU5tpFWZHhlo S3FCeUumaZ8t7k+/TZpkz0MvVnT51ZmrVmjrpTCi9PLxNrdsN2eWs+MLumNYmc+PS3TNz8Vfbhyy 8Ve1dsPFEoxfJoZuX1Vo+nbfdpsvyzbjufij8UlEUlE/lWViilAsFYhBEYIfJ/G4KyIiqffpRpSi lL0hWgP/HwkgPAejuSKklA5RCh/zQt5dH/6qgetGpjj5zpNB3Fgt0Dy73DoHdQkdlJFBRTQnOovh 3xwMCQOWO8UqKYHI8C6ixIESZmc6iVMwm6mAyxJaECAOrDlVx0s3LruWj1s3c20FE6flM27/BE33 avr6q8csPR0rPHDLLp6UW8YYVcvpqyVZtZ4WfGtVOt1xmUaqszm7zzjJlozf8ztSqq7jNRmxu17Z njpw9+0ynE6Txm9eZMVs88ZD9ZJBrBZxPpytalG95dlWzhr0UcL1dvcnTVwmrh3m1MjKyzZPN27B w0WXLuukvrMfiSzZwvty6ZNzOM2Szl0sobtGT+0narNfizfpo/cRMvr1yw25Y8zLaqR2cr/GblM7 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 h81lveBr+CVt0s2PeHwDXMU2/XhYmIstU7k+ozHNn8g+QUu239bbUtbLdkvD5QZE4Val9piHWW0N OQheMJ4pwXxPG6Nd7qYK1W0nwjxaYq981uiwvT2BAaCDkqlMRmIXUnXaxcLSrtifMDsDOzHJXxmR dAMyxBtnlZfk1yBasl25Pvkp17ciHcDGLcpQfHNbrKKnQGSUNwzmz6XaOI2T5hwzmy/gP0D0P4QP nPPnPjPn4/rCvZ+Ds1SzzXovZms7rD+oA2YYVOkCCyJAB1QBAUmMptftKas77aLiiGyNyHc2eYDA KwPYBsQIAgC6gt2+yIINeieriHc7A05O8SFDQTJ74oQzhuTioM3Bldo7cvGdvGQkrtYFwAhk9Y84 +hedD3OhhEe0wKlXOp/Z/VTuiaV2Vc590cjdHUn6nMqJjef4mwq1nE0Q8EaC79Oen2/2HtWnuMIj 1uc5T4dvK1Xqntk6UpcqCqsqR9ah5+cT9WTY653q/z2a96pO3AXOC1KEIqnCoRf00FNIK9QKgLCq EXxq1SGpdXvEV9syaSXX9wXrEo/vAO1tjrNKv34C7tdcj1WucRfGX8gXu2cxspT+YO+qxq/FqWMT 7xPub/06pYUtBN1/QHmGnq137jueIXZn0Sb/DMtS1IHGuShnF5rZI6Svhq0o/Osmp/kOgz7egIHx D4DhI8lS2oZqoiwd9yciqIvMyLuCUYIebvD89QLtb7LNbg9rssJrXjp3ApXXYk0o99cYMW3po9lY GgClh8Lrlh623OxIh6AAhECPFdEn0BmRmY6A0MX5P7jx2tvrrwkuB4LG1obs7QW5ZvcIXANCiwrk PQDYCzJc6V1oGKcYKCPIqqV2219O9uRuy1fc749SfPi9oekipxiiQgmWk8WBZSzydjnXng/zaDqT zizR3IyqK7KR6xWqCCuvqepZVaUMv/0sDOG8yiVEU7GkewOBmGwh3vxySAeg/ifNS2ylspbKWltL bKWltLaW0tpbS2ltLbKWltLaW0tpbSsbSolpbZS0tpVtlLS2ltLaW0tpbZS0tpbZS0tpbS2ltLaW 0q2ylpbS2ltLaW0tpbS2ltLaW0tpbS2ltKiWylpbSolspaW0qJaW0qolspaW0tpbS2ylpbS2ltLa W0tpbS2ltLaW0tpbS2ltLaW0tpbZS0qJbKWltLbKWltLbKWlVbS2ylpbS2lW0tpbZS0q2ltlLS2l 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