diff --git a/extensions-builtin/mobile/javascript/mobile.js b/extensions-builtin/mobile/javascript/mobile.js index 12cae4b7576..3784b65c71a 100644 --- a/extensions-builtin/mobile/javascript/mobile.js +++ b/extensions-builtin/mobile/javascript/mobile.js @@ -18,7 +18,13 @@ function reportWindowSize() { for (var tab of ["txt2img", "img2img"]) { var button = gradioApp().getElementById(tab + '_generate_box'); - var target = gradioApp().getElementById(currentlyMobile ? tab + '_results' : tab + '_actions_column'); + var target = null; + if (currentlyMobile) { + target = gradioApp().getElementById(tab + '_results'); + } else { + target = gradioApp().getElementById(tab + '_actions_column') || gradioApp().getElementById(tab + '_generate_row'); + } + target.insertBefore(button, target.firstElementChild); } } diff --git a/modules/shared_options.py b/modules/shared_options.py index 8630d4741ca..9b12191d2b2 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -232,6 +232,8 @@ "localization": OptionInfo("None", "Localization", gr.Dropdown, lambda: {"choices": ["None"] + list(localization.localizations.keys())}, refresh=lambda: localization.list_localizations(cmd_opts.localizations_dir)).needs_reload_ui(), "gradio_theme": OptionInfo("Default", "Gradio theme", ui_components.DropdownEditable, lambda: {"choices": ["Default"] + shared_gradio_themes.gradio_hf_hub_themes}).info("you can also manually enter any of themes from the gallery.").needs_reload_ui(), "gradio_themes_cache": OptionInfo(True, "Cache gradio themes locally").info("disable to update the selected Gradio theme"), + "move_toprow_to_settings_column": OptionInfo(False, "Move top row (prompt, generate button) to settings column").needs_reload_ui(), + "sticky_generate_button": OptionInfo(False, "Make generate button stick during scroll").info("only applies when the setting above is ticked").needs_reload_ui(), "gallery_height": OptionInfo("", "Gallery height", gr.Textbox).info("an be any valid CSS value").needs_reload_ui(), "return_grid": OptionInfo(True, "Show grid in results for web"), "do_not_show_images": OptionInfo(False, "Do not show any images in results for web"), diff --git a/modules/ui.py b/modules/ui.py index 01f77849d25..9f4b99b1122 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -4,6 +4,7 @@ import sys from functools import reduce import warnings +from contextlib import nullcontext import gradio as gr import gradio.utils @@ -169,69 +170,58 @@ def update_token_counter(text, steps): return f"{token_count}/{max_length}" +class ToprowPrompt: + """Creates prompt components for Toprow""" + def __init__(self, id_part): + self.prompt = gr.Textbox(label="Prompt", elem_id=f"{id_part}_prompt", show_label=False, lines=3, placeholder="Prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) + self.negative_prompt = gr.Textbox(label="Negative prompt", elem_id=f"{id_part}_neg_prompt", show_label=False, lines=3, placeholder="Negative prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) + + class Toprow: """Creates a top row UI with prompts, generate button, styles, extra little buttons for things, and enables some functionality related to their operation""" - def __init__(self, is_img2img): + def __init__(self, is_img2img: bool, toprow_prompt: ToprowPrompt, in_settings_column=False): id_part = "img2img" if is_img2img else "txt2img" self.id_part = id_part + self.in_settings_column = in_settings_column + self.prompt = toprow_prompt.prompt + self.negative_prompt = toprow_prompt.negative_prompt + self.flex_revert = ["flex-basis-revert"] if in_settings_column else [] + + if in_settings_column and opts.sticky_generate_button: + with gr.Row(elem_id=f"{id_part}_generate_row", elem_classes=["generate-sticky"] + self.flex_revert): + self._create_generate_box() with gr.Row(elem_id=f"{id_part}_toprow", variant="compact"): + if in_settings_column and not opts.sticky_generate_button: + with gr.Row(elem_id=f"{id_part}_generate_row", elem_classes=self.flex_revert): + self._create_generate_box() + with gr.Column(elem_id=f"{id_part}_prompt_container", scale=6): - with gr.Row(): - with gr.Column(scale=80): - with gr.Row(): - self.prompt = gr.Textbox(label="Prompt", elem_id=f"{id_part}_prompt", show_label=False, lines=3, placeholder="Prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) - self.prompt_img = gr.File(label="", elem_id=f"{id_part}_prompt_image", file_count="single", type="binary", visible=False) + with gr.Column() if self.in_settings_column else nullcontext(): + with gr.Row(): + toprow_prompt.prompt.render() + self.prompt_img = gr.File(label="", elem_id=f"{id_part}_prompt_image", file_count="single", type="binary", visible=False) - with gr.Row(): - with gr.Column(scale=80): - with gr.Row(): - self.negative_prompt = gr.Textbox(label="Negative prompt", elem_id=f"{id_part}_neg_prompt", show_label=False, lines=3, placeholder="Negative prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) + with gr.Row(): + self.negative_prompt.render() self.button_interrogate = None self.button_deepbooru = None if is_img2img: - with gr.Column(scale=1, elem_classes="interrogate-col"): + with gr.Column(scale=1, elem_classes="interrogate-col") if not in_settings_column else gr.Row(): self.button_interrogate = gr.Button('Interrogate\nCLIP', elem_id="interrogate") self.button_deepbooru = gr.Button('Interrogate\nDeepBooru', elem_id="deepbooru") - with gr.Column(scale=1, elem_id=f"{id_part}_actions_column"): - with gr.Row(elem_id=f"{id_part}_generate_box", elem_classes="generate-box"): - self.interrupt = gr.Button('Interrupt', elem_id=f"{id_part}_interrupt", elem_classes="generate-box-interrupt") - self.skip = gr.Button('Skip', elem_id=f"{id_part}_skip", elem_classes="generate-box-skip") - self.submit = gr.Button('Generate', elem_id=f"{id_part}_generate", variant='primary') - - self.skip.click( - fn=lambda: shared.state.skip(), - inputs=[], - outputs=[], - ) - - self.interrupt.click( - fn=lambda: shared.state.interrupt(), - inputs=[], - outputs=[], - ) - - with gr.Row(elem_id=f"{id_part}_tools"): - self.paste = ToolButton(value=paste_symbol, elem_id="paste") - self.clear_prompt_button = ToolButton(value=clear_prompt_symbol, elem_id=f"{id_part}_clear_prompt") - self.restore_progress_button = ToolButton(value=restore_progress_symbol, elem_id=f"{id_part}_restore_progress", visible=False) - - self.token_counter = gr.HTML(value="0/75", elem_id=f"{id_part}_token_counter", elem_classes=["token-counter"]) - self.token_button = gr.Button(visible=False, elem_id=f"{id_part}_token_button") - self.negative_token_counter = gr.HTML(value="0/75", elem_id=f"{id_part}_negative_token_counter", elem_classes=["token-counter"]) - self.negative_token_button = gr.Button(visible=False, elem_id=f"{id_part}_negative_token_button") - - self.clear_prompt_button.click( - fn=lambda *x: x, - _js="confirm_clear_prompt", - inputs=[self.prompt, self.negative_prompt], - outputs=[self.prompt, self.negative_prompt], - ) + if in_settings_column: + with gr.Row(elem_classes=self.flex_revert): + self._create_styles(extra_buttons=self._create_tools) - self.ui_styles = ui_prompt_styles.UiPromptStyles(id_part, self.prompt, self.negative_prompt) + if not in_settings_column: + with gr.Column(scale=1, elem_id=f"{id_part}_actions_column"): + self._create_generate_box() + self._create_tools() + self._create_styles() self.prompt_img.change( fn=modules.images.image_data, @@ -240,6 +230,46 @@ def __init__(self, is_img2img): show_progress=False, ) + def _create_generate_box(self): + with gr.Row(elem_id=f"{self.id_part}_generate_box", elem_classes=["generate-box"] + self.flex_revert): + self.interrupt = gr.Button('Interrupt', elem_id=f"{self.id_part}_interrupt", elem_classes="generate-box-interrupt") + self.skip = gr.Button('Skip', elem_id=f"{self.id_part}_skip", elem_classes="generate-box-skip") + self.submit = gr.Button('Generate', elem_id=f"{self.id_part}_generate", variant='primary') + + self.skip.click( + fn=lambda: shared.state.skip(), + inputs=[], + outputs=[], + ) + + self.interrupt.click( + fn=lambda: shared.state.interrupt(), + inputs=[], + outputs=[], + ) + + def _create_tools(self): + with gr.Row(elem_id=f"{self.id_part}_tools", elem_classes=self.flex_revert) if not self.in_settings_column else nullcontext(): + self.paste = ToolButton(value=paste_symbol, elem_id="paste") + self.clear_prompt_button = ToolButton(value=clear_prompt_symbol, elem_id=f"{self.id_part}_clear_prompt") + self.restore_progress_button = ToolButton(value=restore_progress_symbol, elem_id=f"{self.id_part}_restore_progress", visible=False) + + self.token_counter = gr.HTML(value="0/75", elem_id=f"{self.id_part}_token_counter", elem_classes=["token-counter"]) + self.token_button = gr.Button(visible=False, elem_id=f"{self.id_part}_token_button") + self.negative_token_counter = gr.HTML(value="0/75", elem_id=f"{self.id_part}_negative_token_counter", elem_classes=["token-counter"]) + self.negative_token_button = gr.Button(visible=False, elem_id=f"{self.id_part}_negative_token_button") + + self.clear_prompt_button.click( + fn=lambda *x: x, + _js="confirm_clear_prompt", + inputs=[self.prompt, self.negative_prompt], + outputs=[self.prompt, self.negative_prompt], + ) + + def _create_styles(self, extra_buttons=None): + with gr.Row(elem_classes=self.flex_revert) if self.in_settings_column else nullcontext(): + self.ui_styles = ui_prompt_styles.UiPromptStyles(self.id_part, self.prompt, self.negative_prompt, extra_buttons=extra_buttons) + def setup_progressbar(*args, **kwargs): pass @@ -325,8 +355,10 @@ def create_ui(): scripts.scripts_current = scripts.scripts_txt2img scripts.scripts_txt2img.initialize_scripts(is_img2img=False) + txt2img_prompt = ToprowPrompt("txt2img") with gr.Blocks(analytics_enabled=False) as txt2img_interface: - toprow = Toprow(is_img2img=False) + if not opts.move_toprow_to_settings_column: + toprow = Toprow(is_img2img=False, toprow_prompt=txt2img_prompt) dummy_component = gr.Label(visible=False) @@ -335,6 +367,9 @@ def create_ui(): with gr.Tab("Generation", id="txt2img_generation") as txt2img_generation_tab, gr.Row(equal_height=False): with gr.Column(variant='compact', elem_id="txt2img_settings"): + if opts.move_toprow_to_settings_column: + toprow = Toprow(is_img2img=False, toprow_prompt=txt2img_prompt, in_settings_column=True) + scripts.scripts_txt2img.prepare_ui() for category in ordered_ui_categories(): @@ -543,14 +578,19 @@ def create_ui(): scripts.scripts_current = scripts.scripts_img2img scripts.scripts_img2img.initialize_scripts(is_img2img=True) + img2img_prompt = ToprowPrompt("img2img") with gr.Blocks(analytics_enabled=False) as img2img_interface: - toprow = Toprow(is_img2img=True) + if not opts.move_toprow_to_settings_column: + toprow = Toprow(is_img2img=True, toprow_prompt=img2img_prompt) extra_tabs = gr.Tabs(elem_id="img2img_extra_tabs") extra_tabs.__enter__() with gr.Tab("Generation", id="img2img_generation") as img2img_generation_tab, FormRow(equal_height=False): with gr.Column(variant='compact', elem_id="img2img_settings"): + if opts.move_toprow_to_settings_column: + toprow = Toprow(is_img2img=True, toprow_prompt=img2img_prompt, in_settings_column=True) + copy_image_buttons = [] copy_image_destinations = {} diff --git a/modules/ui_prompt_styles.py b/modules/ui_prompt_styles.py index 85eb3a6417e..e911ca08108 100644 --- a/modules/ui_prompt_styles.py +++ b/modules/ui_prompt_styles.py @@ -50,12 +50,14 @@ def refresh_styles(): class UiPromptStyles: - def __init__(self, tabname, main_ui_prompt, main_ui_negative_prompt): + def __init__(self, tabname, main_ui_prompt, main_ui_negative_prompt, extra_buttons=None): self.tabname = tabname with gr.Row(elem_id=f"{tabname}_styles_row"): self.dropdown = gr.Dropdown(label="Styles", show_label=False, elem_id=f"{tabname}_styles", choices=list(shared.prompt_styles.styles), value=[], multiselect=True, tooltip="Styles") edit_button = ui_components.ToolButton(value=styles_edit_symbol, elem_id=f"{tabname}_styles_edit_button", tooltip="Edit styles") + if extra_buttons is not None: + extra_buttons() with gr.Box(elem_id=f"{tabname}_styles_dialog", elem_classes="popup-dialog") as styles_dialog: with gr.Row(): diff --git a/style.css b/style.css index 38a01e72d39..6f9100707a6 100644 --- a/style.css +++ b/style.css @@ -142,6 +142,10 @@ div.gradio-container, .block.gradio-textbox, div.gradio-group, div.gradio-dropdo overflow: visible !important; } +.flex-basis-revert { + flex-basis: revert !important; +} + /* general styled components */ @@ -322,6 +326,12 @@ div.block.gradio-accordion { right: 0; border-radius: 0 0.5rem 0.5rem 0; } +.generate-sticky{ + position: sticky; + z-index: 500; + top: 0.5em; + margin-bottom: 1em; +} #img2img_scale_resolution_preview.block{ display: flex;